<?php

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

/**
 * Selects product cards that are explicitly grounded in the model answer.
 */
class Niwa_AI_Frontend_Product_Card_Selector {
	private const MAX_GRID_PRODUCTS = 4;

	private $card_builder;
	private $catalog_provider;

	public function __construct( $card_builder = null, $catalog_provider = null ) {
		$this->card_builder    = $card_builder;
		$this->catalog_provider = $catalog_provider;
	}

	public function select_cards( $answer, $tool_payloads ) {
		$product_ids = $this->resolve_grounded_product_ids( $answer, $tool_payloads );
		$references_by_id = array();
		foreach ( $this->collect_grounded_references( $tool_payloads ) as $reference ) {
			$references_by_id[ intval( $reference['id'] ?? 0 ) ] = $reference;
		}
		$previews = array();
		foreach ( $product_ids as $product_id ) {
			$previews[ $product_id ] = isset( $references_by_id[ $product_id ]['variation_preview'] )
				? (array) $references_by_id[ $product_id ]['variation_preview']
				: array();
		}

		$match_metadata = array();
		foreach ( $product_ids as $product_id ) {
			if ( isset( $references_by_id[ $product_id ] ) ) {
				$match_metadata[ $product_id ] = $references_by_id[ $product_id ];
			}
		}

		return $this->build_cards_for_product_ids( $product_ids, $previews, $match_metadata );
	}

	/**
	 * Resolve only IDs that are explicitly grounded in the final answer.
	 *
	 * This is the safe boundary used before lazy WooCommerce hydration: callers
	 * can hydrate the returned finalists without loading every search candidate.
	 *
	 * @return int[] Up to four product IDs in answer order.
	 */
	public function resolve_grounded_product_ids( $answer, $tool_payloads ) {
		if ( '' === trim( (string) $answer ) ) {
			return array();
		}

		// A real catalog title is not sufficient grounding by itself: the model
		// can hallucinate that exact title. Only candidates returned by the tools
		// in this answer run may trigger live WooCommerce hydration and a card.
		$references = $this->collect_grounded_references( $tool_payloads );
		if ( empty( $references ) ) {
			return array();
		}

		$mentioned_ids = $this->resolve_mentioned_product_ids( $answer, $references );
		return $this->normalize_grid_product_ids( $mentioned_ids );
	}

	/** Build cards for IDs that the caller already resolved as grounded. */
	public function build_cards_for_product_ids( $product_ids, $variation_previews = array(), $match_metadata = array() ) {
		$cards = array();
		foreach ( array_slice( array_values( array_unique( array_map( 'intval', (array) $product_ids ) ) ), 0, self::MAX_GRID_PRODUCTS ) as $product_id ) {
			if ( $product_id <= 0 ) {
				continue;
			}
			$meta = isset( $match_metadata[ $product_id ] ) && is_array( $match_metadata[ $product_id ] ) ? $match_metadata[ $product_id ] : array();
			if ( ( array_key_exists( 'card_eligible', $meta ) && empty( $meta['card_eligible'] ) ) || 'rejected' === ( isset( $meta['match_class'] ) ? $meta['match_class'] : '' ) ) {
				continue;
			}
			$preview = isset( $variation_previews[ $product_id ] ) && is_array( $variation_previews[ $product_id ] )
				? $variation_previews[ $product_id ]
				: array();
			$card = $this->card_builder ? $this->card_builder->build_card_from_id( $product_id, $preview ) : array();
			if ( ! empty( $card ) ) {
				foreach ( array( 'match_class', 'disclosure_required', 'matched_constraints', 'missing_constraints', 'conflicting_constraints' ) as $match_key ) {
					if ( array_key_exists( $match_key, $meta ) ) {
						$card[ $match_key ] = $meta[ $match_key ];
					}
				}
				$cards[] = $card;
			}
		}

		return $cards;
	}

	/**
	 * Enforce the supported recommendation grid sizes.
	 *
	 * @param int[] $mentioned_ids Product IDs grounded in the answer.
	 * @return int[] Up to four product IDs.
	 */
	private function normalize_grid_product_ids( $mentioned_ids ) {
		$mentioned_ids = array_values( array_unique( array_map( 'intval', (array) $mentioned_ids ) ) );
		return array_slice( $mentioned_ids, 0, self::MAX_GRID_PRODUCTS );
	}

	private function collect_grounded_references( $tool_payloads ) {
		$references = array();
		foreach ( (array) $tool_payloads as $payload ) {
			if ( empty( $payload['_card_refs'] ) || ! is_array( $payload['_card_refs'] ) ) {
				continue;
			}

			foreach ( $payload['_card_refs'] as $reference ) {
				if ( ! is_array( $reference ) ) {
					continue;
				}
				if ( ( array_key_exists( 'card_eligible', $reference ) && empty( $reference['card_eligible'] ) ) || 'rejected' === ( isset( $reference['match_class'] ) ? $reference['match_class'] : '' ) ) {
					continue;
				}

				$product_id = isset( $reference['id'] ) ? intval( $reference['id'] ) : 0;
				$title      = isset( $reference['title'] ) ? trim( (string) $reference['title'] ) : '';
				$url        = isset( $reference['url'] ) ? trim( (string) $reference['url'] ) : '';
				if ( $product_id <= 0 || ( '' === $title && '' === $url ) ) {
					continue;
				}

				$incoming = array(
					'id'     => $product_id,
					'title'  => $title,
					'url'    => $url,
					'score'  => isset( $reference['score'] ) ? floatval( $reference['score'] ) : 0.0,
					'source' => isset( $reference['source'] ) ? (string) $reference['source'] : '',
				);
				foreach ( array( 'match_class', 'disclosure_required', 'matched_constraints', 'missing_constraints', 'conflicting_constraints', 'constraint_evidence' ) as $match_key ) {
					if ( array_key_exists( $match_key, $reference ) ) {
						$incoming[ $match_key ] = $reference[ $match_key ];
					}
				}
				if ( ! empty( $reference['variation_preview'] ) && is_array( $reference['variation_preview'] ) ) {
					$incoming['variation_preview'] = $reference['variation_preview'];
				}

				if ( isset( $references[ $product_id ] ) ) {
					$existing_class = isset( $references[ $product_id ]['match_class'] ) ? $references[ $product_id ]['match_class'] : '';
					$incoming_class = isset( $incoming['match_class'] ) ? $incoming['match_class'] : '';
					$existing_priority = class_exists( 'Niwa_AI_Frontend_Product_Query_Plan' )
						? Niwa_AI_Frontend_Product_Query_Plan::class_priority( $existing_class )
						: ( '' !== $existing_class ? 0 : 9 );
					$incoming_priority = class_exists( 'Niwa_AI_Frontend_Product_Query_Plan' )
						? Niwa_AI_Frontend_Product_Query_Plan::class_priority( $incoming_class )
						: ( '' !== $incoming_class ? 0 : 9 );
					if ( $existing_priority < $incoming_priority ) {
						continue;
					}
					$incoming = array_merge( $references[ $product_id ], $incoming );
				}
				$references[ $product_id ] = $incoming;
			}
		}

		return array_values( $references );
	}

	private function resolve_mentioned_product_ids( $answer, $references ) {
		$answer_normalized = ' ' . $this->normalize_text( $answer ) . ' ';
		$answer_urls       = $this->extract_urls( $answer );
		$title_owners      = array();
		$url_owners        = array();
		$code_owners       = array();

		foreach ( $references as $reference ) {
			$product_id = intval( $reference['id'] );
			$title      = $this->normalize_text( $reference['title'] );
			$url        = $this->normalize_url( $reference['url'] );

			if ( '' !== $title ) {
				$title_owners[ $title ][] = $product_id;
				foreach ( $this->tokenize( $title ) as $token ) {
					if ( mb_strlen( $token ) >= 3 && preg_match( '/\p{N}/u', $token ) ) {
						$code_owners[ $token ][ $product_id ] = true;
					}
				}
			}
			if ( '' !== $url ) {
				$url_owners[ $url ][] = $product_id;
			}
		}

		$matches = $this->resolve_unambiguous_title_mentions( $answer_normalized, $title_owners );
		foreach ( $references as $reference ) {
			$product_id = intval( $reference['id'] );
			$title      = $this->normalize_text( $reference['title'] );
			$url        = $this->normalize_url( $reference['url'] );

			if ( '' !== $url && 1 === count( $url_owners[ $url ] ?? array() ) ) {
				if ( isset( $answer_urls[ $url ] ) ) {
					$matches[ $product_id ] = intval( $answer_urls[ $url ] );
				}
			}

			foreach ( $this->tokenize( $title ) as $token ) {
				if ( isset( $code_owners[ $token ] ) && 1 === count( $code_owners[ $token ] ) ) {
					$code_position = mb_stripos( $answer_normalized, ' ' . $token . ' ' );
					if ( false !== $code_position ) {
						$matches[ $product_id ] = isset( $matches[ $product_id ] ) ? min( $matches[ $product_id ], intval( $code_position ) ) : intval( $code_position );
					}
				}
			}
		}

		asort( $matches, SORT_NUMERIC );
		return array_slice( array_map( 'intval', array_keys( $matches ) ), 0, self::MAX_GRID_PRODUCTS );
	}

	private function resolve_unambiguous_title_mentions( $answer_normalized, $title_owners ) {
		$mentions = array();
		foreach ( $title_owners as $title => $owners ) {
			if ( '' === $title || 1 !== count( $owners ) ) {
				continue;
			}

			$needle = ' ' . $title . ' ';
			$offset = 0;
			while ( false !== ( $position = mb_stripos( $answer_normalized, $needle, $offset ) ) ) {
				$start      = intval( $position ) + 1;
				$mentions[] = array(
					'product_id' => intval( $owners[0] ),
					'position'   => $start,
					'end'        => $start + mb_strlen( $title ),
					'length'     => mb_strlen( $title ),
				);
				$offset = intval( $position ) + max( 1, mb_strlen( $needle ) );
			}
		}

		$matches = array();
		foreach ( $mentions as $index => $mention ) {
			$blocked = false;
			foreach ( $mentions as $other_index => $other ) {
				if ( $index === $other_index || $mention['product_id'] === $other['product_id'] ) {
					continue;
				}
				$overlaps = $mention['position'] < $other['end'] && $other['position'] < $mention['end'];
				if ( $overlaps && $other['length'] >= $mention['length'] ) {
					$blocked = true;
					break;
				}
			}
			if ( $blocked ) {
				continue;
			}

			$product_id = intval( $mention['product_id'] );
			$position   = intval( $mention['position'] );
			$matches[ $product_id ] = isset( $matches[ $product_id ] ) ? min( $matches[ $product_id ], $position ) : $position;
		}

		return $matches;
	}

	private function extract_urls( $text ) {
		if ( ! preg_match_all( '~https?://[^\s<>\[\]()"\']+~iu', (string) $text, $matches, PREG_OFFSET_CAPTURE ) ) {
			return array();
		}

		$urls = array();
		foreach ( $matches[0] as $match ) {
			$url        = isset( $match[0] ) ? (string) $match[0] : '';
			$position   = isset( $match[1] ) ? intval( $match[1] ) : 0;
			$normalized = $this->normalize_url( rtrim( $url, '.,;:!?' ) );
			if ( '' !== $normalized && ! isset( $urls[ $normalized ] ) ) {
				$urls[ $normalized ] = intval( $position );
			}
		}

		return $urls;
	}

	private function normalize_url( $url ) {
		$url   = html_entity_decode( trim( (string) $url ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$parts = parse_url( $url );
		if ( ! is_array( $parts ) || empty( $parts['host'] ) ) {
			return '';
		}

		$scheme = ! empty( $parts['scheme'] ) ? strtolower( (string) $parts['scheme'] ) : 'https';
		$host   = strtolower( (string) $parts['host'] );
		$port   = ! empty( $parts['port'] ) ? ':' . intval( $parts['port'] ) : '';
		$path   = isset( $parts['path'] ) ? rawurldecode( (string) $parts['path'] ) : '';

		return rtrim( $scheme . '://' . $host . $port . $path, '/' );
	}

	private function tokenize( $text ) {
		$text = $this->normalize_text( $text );
		return '' === $text ? array() : array_values( array_unique( explode( ' ', $text ) ) );
	}

	private function normalize_text( $text ) {
		$text = html_entity_decode( mb_strtolower( (string) $text ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		if ( function_exists( 'remove_accents' ) ) {
			$text = remove_accents( $text );
		}
		$text = preg_replace( '/[^\p{L}\p{N}\s]/u', ' ', $text );
		$text = preg_replace( '/\s+/u', ' ', (string) $text );
		return trim( (string) $text );
	}
}
