<?php

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

/**
 * Builds searchable WooCommerce product evidence from real attributes and variations.
 */
class Niwa_AI_Product_Search_Evidence {
	private const MAX_VARIATIONS = 200;
	private const CACHE_VERSION = 4;
	private static $runtime_cache = array();

	/**
	 * @param int|WC_Product $product Product ID or object.
	 * @return array{
	 *     attributes_text:string,
	 *     attributes_norm:string,
	 *     variations:array<int,array<string,mixed>>,
	 *     structured_claims:array<int,array<string,mixed>>,
	 *     provenance:array<int,array<string,mixed>>,
	 *     coverage:array<string,mixed>
	 * }
	 */
	public static function build( $product ) {
		if ( ! is_object( $product ) && function_exists( 'wc_get_product' ) ) {
			$product = wc_get_product( intval( $product ) );
		}
		if ( ! $product || ! method_exists( $product, 'get_id' ) ) {
			return self::empty_result();
		}
		$cache_key = self::cache_key( $product );
		if ( isset( self::$runtime_cache[ $cache_key ] ) ) {
			return self::$runtime_cache[ $cache_key ];
		}
		if ( function_exists( 'get_transient' ) ) {
			$cached = get_transient( $cache_key );
			if ( is_array( $cached ) ) {
				self::$runtime_cache[ $cache_key ] = $cached;
				return $cached;
			}
		}

		$product_id        = intval( $product->get_id() );
		$structured_claims = array();
		$provenance        = array(
			array(
				'source_kind' => 'woocommerce_product',
				'source_ref'  => 'product:' . $product_id,
				'product_id'  => $product_id,
			),
		);
		$parent_values    = self::collect_parent_attributes( $product, $structured_claims, $provenance );
		$available_values = array();
		$all_values       = array();
		$variations       = array();
		$variation_ids    = array();
		$processed_count  = 0;

		if ( method_exists( $product, 'is_type' ) && $product->is_type( 'variable' ) && method_exists( $product, 'get_children' ) ) {
			$variation_ids   = array_values( array_filter( array_map( 'intval', (array) $product->get_children() ) ) );
			$variation_slice = array_slice( $variation_ids, 0, self::MAX_VARIATIONS );
			$processed_count = count( $variation_slice );
			foreach ( $variation_slice as $variation_id ) {
				$variation = function_exists( 'wc_get_product' ) ? wc_get_product( intval( $variation_id ) ) : null;
				if ( ! $variation || ! method_exists( $variation, 'is_type' ) || ! $variation->is_type( 'variation' ) ) {
					continue;
				}
				if ( method_exists( $variation, 'get_status' ) && ! in_array( (string) $variation->get_status(), array( 'publish', 'private' ), true ) ) {
					continue;
				}

				$attribute_sources   = array();
				$wildcard_attributes = array();
				$attributes = self::resolve_variation_attributes( $variation, $attribute_sources, $wildcard_attributes );
				if ( empty( $attributes ) && empty( $wildcard_attributes ) ) {
					continue;
				}

				$in_stock   = ! method_exists( $variation, 'is_in_stock' ) || $variation->is_in_stock();
				$purchasable = ! method_exists( $variation, 'is_purchasable' ) || $variation->is_purchasable();
				foreach ( $attribute_sources as $source ) {
					$claim = self::build_structured_claim(
						'variation',
						intval( $variation->get_id() ),
						isset( $source['attribute_name'] ) ? (string) $source['attribute_name'] : '',
						isset( $source['attribute_label'] ) ? (string) $source['attribute_label'] : '',
						isset( $source['resolved_value'] ) ? (string) $source['resolved_value'] : '',
						$in_stock && $purchasable ? 'available_variation' : 'variation',
						$source
					);
					if ( ! empty( $claim ) ) {
						$structured_claims[] = $claim;
						$provenance[]        = $source;
					}
				}
				foreach ( $attributes as $label => $value ) {
					self::append_value( $all_values, $label, $value );
					if ( $in_stock && $purchasable ) {
						self::append_value( $available_values, $label, $value );
					}
				}

				$image_id   = method_exists( $variation, 'get_image_id' ) ? intval( $variation->get_image_id() ) : 0;
				$image_data = $image_id && function_exists( 'wp_get_attachment_image_src' )
					? wp_get_attachment_image_src( $image_id, 'woocommerce_thumbnail' )
					: false;

				$variations[] = array(
					'variation_id' => intval( $variation->get_id() ),
					'attributes'   => $attributes,
					'wildcard_attributes' => $wildcard_attributes,
					'selection'    => method_exists( $variation, 'get_variation_attributes' ) ? (array) $variation->get_variation_attributes() : array(),
					'image'        => is_array( $image_data ) && ! empty( $image_data[0] ) ? (string) $image_data[0] : '',
					'image_width'  => is_array( $image_data ) && isset( $image_data[1] ) ? intval( $image_data[1] ) : 0,
					'image_height' => is_array( $image_data ) && isset( $image_data[2] ) ? intval( $image_data[2] ) : 0,
					'stock_status' => method_exists( $variation, 'get_stock_status' ) ? (string) $variation->get_stock_status() : ( $in_stock ? 'instock' : 'outofstock' ),
					'purchasable'  => (bool) ( $in_stock && $purchasable ),
				);
			}
		}

		$parts = array();
		if ( ! empty( $parent_values ) ) {
			$parts[] = self::format_attribute_group( 'Product attributes', $parent_values );
		}
		if ( ! empty( $available_values ) ) {
			$parts[] = self::format_attribute_group( 'Available variation values', $available_values );
		}
		if ( ! empty( $all_values ) && $all_values !== $available_values ) {
			$parts[] = self::format_attribute_group( 'All variation values', $all_values );
		}

		$text = implode( '; ', array_filter( $parts ) );
		$variation_count = count( $variation_ids );
		$truncated       = $variation_count > self::MAX_VARIATIONS;
		$result = array(
			'attributes_text' => $text,
			'attributes_norm' => self::normalize( $text ),
			'variations'      => $variations,
			'structured_claims' => $structured_claims,
			'provenance'        => $provenance,
			'coverage'          => array(
				'source_available' => true,
				'variation_count'  => $variation_count,
				'processed_count'  => $processed_count,
				'indexed_count'    => count( $variations ),
				'max_variations'   => self::MAX_VARIATIONS,
				'truncated'        => $truncated,
				'complete'         => ! $truncated,
				'completeness'     => $truncated ? 'truncated' : 'complete',
				'coverage_ratio'   => $variation_count > 0
					? min( 1.0, $processed_count / $variation_count )
					: 1.0,
			),
		);
		self::$runtime_cache[ $cache_key ] = $result;
		if ( function_exists( 'set_transient' ) ) {
			set_transient( $cache_key, $result, 15 * MINUTE_IN_SECONDS );
		}
		return $result;
	}

	/**
	 * Clear the evidence cache before a parent product is reindexed after a
	 * variation or taxonomy relationship change. The cache key is based on the
	 * parent timestamp, which WooCommerce does not update for every child edit.
	 *
	 * @param int|WC_Product $product Product ID or object.
	 * @return void
	 */
	public static function invalidate( $product ) {
		if ( ! is_object( $product ) ) {
			if ( ! function_exists( 'wc_get_product' ) ) {
				return;
			}
			$product = wc_get_product( intval( $product ) );
		}
		if ( ! $product || ! method_exists( $product, 'get_id' ) ) {
			return;
		}

		$cache_key = self::cache_key( $product );
		unset( self::$runtime_cache[ $cache_key ] );
		if ( function_exists( 'delete_transient' ) ) {
			delete_transient( $cache_key );
		}
	}

	private static function cache_key( $product ) {
		$modified = method_exists( $product, 'get_date_modified' ) ? $product->get_date_modified() : null;
		$stamp    = $modified && method_exists( $modified, 'getTimestamp' ) ? intval( $modified->getTimestamp() ) : 0;
		return 'niwa_ai_product_search_' . self::CACHE_VERSION . '_' . intval( $product->get_id() ) . '_' . $stamp;
	}

	private static function collect_parent_attributes( $product, &$structured_claims = array(), &$provenance = array() ) {
		$out = array();
		if ( ! method_exists( $product, 'get_attributes' ) ) {
			return $out;
		}

		foreach ( (array) $product->get_attributes() as $attribute ) {
			if ( ! is_object( $attribute ) || ! method_exists( $attribute, 'get_name' ) ) {
				continue;
			}
			$name  = (string) $attribute->get_name();
			$label = function_exists( 'wc_attribute_label' ) ? (string) wc_attribute_label( $name, $product ) : $name;
			$values      = array();
			$is_taxonomy = method_exists( $attribute, 'is_taxonomy' ) && $attribute->is_taxonomy();
			if ( $is_taxonomy && function_exists( 'wc_get_product_terms' ) ) {
				$values = wc_get_product_terms( intval( $product->get_id() ), $name, array( 'fields' => 'all' ) );
				$values = is_wp_error( $values ) ? array() : (array) $values;
			} elseif ( method_exists( $attribute, 'get_options' ) ) {
				$values = (array) $attribute->get_options();
			}
			foreach ( $values as $raw_option ) {
				$term_id   = is_object( $raw_option ) && isset( $raw_option->term_id ) ? intval( $raw_option->term_id ) : 0;
				$term_slug = is_object( $raw_option ) && isset( $raw_option->slug ) ? (string) $raw_option->slug : '';
				$value     = is_object( $raw_option ) && isset( $raw_option->name ) ? (string) $raw_option->name : (string) $raw_option;
				self::append_value( $out, $label, $value );
				$source = array(
					'source_kind'    => $is_taxonomy
						? 'woo_product_attribute_taxonomy'
						: 'woo_product_attribute',
					'source_ref'     => 'product_attribute:' . $name . ( $term_id > 0 ? ':term:' . $term_id : '' ),
					'product_id'     => intval( $product->get_id() ),
					'attribute_name' => $name,
					'attribute_label'=> $label,
					'raw_value'      => $value,
					'resolved_value' => $value,
					'taxonomy'       => $is_taxonomy ? $name : '',
					'term_id'        => $term_id,
					'term_slug'      => $term_slug,
				);
				$claim = self::build_structured_claim(
					'product',
					intval( $product->get_id() ),
					$name,
					$label,
					$value,
					'product',
					$source
				);
				if ( ! empty( $claim ) ) {
					$structured_claims[] = $claim;
					$provenance[]        = $source;
				}
			}
		}

		return $out;
	}

	private static function resolve_variation_attributes( $variation, &$attribute_sources = array(), &$wildcard_attributes = array() ) {
		$out = array();
		if ( ! method_exists( $variation, 'get_attributes' ) ) {
			return $out;
		}

		foreach ( (array) $variation->get_attributes() as $name => $raw_value ) {
			$raw_value = trim( (string) $raw_value );
			$value = $raw_value;
			if ( '' === $value ) {
				$wildcard_attributes[] = (string) $name;
				continue;
			}
			$taxonomy = 0 === strpos( (string) $name, 'pa_' ) ? (string) $name : '';
			$label    = function_exists( 'wc_attribute_label' ) ? (string) wc_attribute_label( (string) $name, $variation ) : (string) $name;
			$term_id  = 0;
			$term_slug = '';
			if ( '' !== $taxonomy && function_exists( 'get_term_by' ) ) {
				$term = get_term_by( 'slug', $value, $taxonomy );
				if ( $term && ! is_wp_error( $term ) && ! empty( $term->name ) ) {
					$value = (string) $term->name;
					$term_id = isset( $term->term_id ) ? intval( $term->term_id ) : 0;
					$term_slug = isset( $term->slug ) ? (string) $term->slug : $raw_value;
				}
			}
			$out[ $label ] = $value;
			$attribute_sources[] = array(
				'source_kind'    => 'woo_variation_attribute',
				'source_ref'     => 'variation_attribute:' . (string) $name,
				'product_id'     => method_exists( $variation, 'get_parent_id' ) ? intval( $variation->get_parent_id() ) : 0,
				'variation_id'   => intval( $variation->get_id() ),
				'attribute_name' => (string) $name,
				'attribute_label'=> $label,
				'raw_value'      => $raw_value,
				'resolved_value' => $value,
				'taxonomy'       => $taxonomy,
				'term_id'        => $term_id,
				'term_slug'      => $term_slug,
			);
		}

		$wildcard_attributes = array_values( array_unique( array_filter( array_map( 'strval', $wildcard_attributes ) ) ) );
		return $out;
	}

	private static function build_structured_claim( $scope, $subject_id, $raw_key, $label, $value, $availability_scope, $source ) {
		$label = trim( (string) $label );
		$value = trim( (string) $value );
		if ( '' === $label || '' === $value ) {
			return array();
		}

		$normalized_key   = self::normalize( '' !== (string) $raw_key ? (string) $raw_key : $label );
		$normalized_value = self::normalize( $value );
		$claim_id         = sha1( implode( '|', array( $scope, intval( $subject_id ), $normalized_key, $normalized_value, $availability_scope ) ) );

		return array(
			'claim_id' => $claim_id,
			'subject'  => array(
				'scope' => (string) $scope,
				'id'    => intval( $subject_id ),
			),
			'frame'    => 'property',
			'predicate'=> array(
				'raw_key'        => (string) $raw_key,
				'label'          => $label,
				'normalized_key' => $normalized_key,
			),
			'object'   => array(
				'raw'        => $value,
				'normalized' => $normalized_value,
				'value_type' => 'text',
			),
			'availability_scope' => (string) $availability_scope,
			'evidence_tier'      => 'structured_catalog',
			'confidence'         => 1.0,
			'provenance'         => array( $source ),
		);
	}

	private static function append_value( &$groups, $label, $value ) {
		$label = trim( (string) $label );
		$value = trim( (string) $value );
		if ( '' === $label || '' === $value ) {
			return;
		}
		if ( ! isset( $groups[ $label ] ) ) {
			$groups[ $label ] = array();
		}
		if ( ! in_array( $value, $groups[ $label ], true ) ) {
			$groups[ $label ][] = $value;
		}
	}

	private static function format_attribute_group( $heading, $groups ) {
		$parts = array();
		foreach ( $groups as $label => $values ) {
			$parts[] = $label . ': ' . implode( '/', (array) $values );
		}
		return $heading . ': ' . implode( ', ', $parts );
	}

	private static function normalize( $text ) {
		$text = function_exists( 'mb_strtolower' ) ? mb_strtolower( (string) $text, 'UTF-8' ) : strtolower( (string) $text );
		if ( function_exists( 'remove_accents' ) ) {
			$text = remove_accents( $text );
		}
		$text = preg_replace( '/[^\p{L}\p{N}\s]+/u', ' ', $text );
		return trim( preg_replace( '/\s+/u', ' ', (string) $text ) );
	}

	private static function empty_result() {
		return array(
			'attributes_text' => '',
			'attributes_norm' => '',
			'variations'      => array(),
			'structured_claims' => array(),
			'provenance'        => array(),
			'coverage'          => array(
				'source_available' => false,
				'variation_count'  => 0,
				'processed_count'  => 0,
				'indexed_count'    => 0,
				'max_variations'   => self::MAX_VARIATIONS,
				'truncated'        => false,
				'complete'         => false,
				'completeness'     => 'unavailable',
				'coverage_ratio'   => 0.0,
			),
		);
	}
}
