<?php
/**
 * Answyr — Document retrieval (docs grounding).
 *
 * Keyword search over the site's knowledge base (native Docs, BetterDocs, Heroic
 * KB, Echo KB, or plain pages). Strips stop-words and broadens to an OR match so
 * question-style queries still surface the right doc. Pairs with the Knowledge
 * Pack to build the AI context.
 *
 * @package Answyr
 */

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

final class Answyr_Retrieval {

	/** @return WP_Post[] */
	public function search( string $query, int $limit = 4 ): array {
		$post_types = $this->get_active_post_types();
		$limit      = (int) apply_filters( 'answyr_max_results', $limit );

		$results = $this->keyword_search( $query, $post_types, $limit );
		if ( empty( $results ) ) {
			$results = $this->fallback_recent( $post_types, $limit );
		}
		return $results;
	}

	/** @param WP_Post[] $posts */
	public function format_context( array $posts ): string {
		if ( empty( $posts ) ) {
			return __( 'No relevant documentation found.', 'answyr' );
		}
		$parts = array();
		foreach ( $posts as $post ) {
			$content = trim( preg_replace( '/\s+/', ' ', wp_strip_all_tags( $post->post_content ) ) );
			$words   = explode( ' ', $content );
			if ( count( $words ) > 800 ) {
				$content = implode( ' ', array_slice( $words, 0, 800 ) ) . '…';
			}
			$parts[] = sprintf( "## %s\nURL: %s\n\n%s", $post->post_title, get_permalink( $post->ID ), $content );
		}
		return implode( "\n\n---\n\n", $parts );
	}

	/**
	 * @param WP_Post[] $posts
	 * @return array<int,array{title:string,url:string}>
	 */
	public function get_sources( array $posts ): array {
		return array_map(
			static function ( WP_Post $post ) {
				return array(
					'title' => $post->post_title,
					'url'   => get_permalink( $post->ID ),
				);
			},
			$posts
		);
	}

	// ── Private ─────────────────────────────────────────────────────────────────

	private function keyword_search( string $query, array $post_types, int $limit ): array {
		$terms = $this->keywords( $query );

		// Primary: relevance search on the meaningful terms (falls back to the raw
		// query if nothing survives stop-word stripping).
		$search  = $terms ? implode( ' ', $terms ) : sanitize_text_field( $query );
		$primary = $this->wp_search( $search, $post_types, $limit );
		if ( $primary || count( $terms ) < 2 ) {
			return $primary;
		}

		// Broaden: the all-words search found nothing, so score posts by how many
		// individual terms they match (OR), and return the best.
		$scores = array();
		$by_id  = array();
		foreach ( $terms as $term ) {
			foreach ( $this->wp_search( $term, $post_types, $limit * 2 ) as $post ) {
				$scores[ $post->ID ] = ( $scores[ $post->ID ] ?? 0 ) + 1;
				$by_id[ $post->ID ]  = $post;
			}
		}
		arsort( $scores );
		$out = array();
		foreach ( array_keys( $scores ) as $id ) {
			$out[] = $by_id[ $id ];
			if ( count( $out ) >= $limit ) {
				break;
			}
		}
		return $out;
	}

	/** Run a single WordPress relevance search. @return WP_Post[] */
	private function wp_search( string $search, array $post_types, int $limit ): array {
		$q = new WP_Query(
			array(
				'post_type'      => $post_types,
				'post_status'    => 'publish',
				's'              => sanitize_text_field( $search ),
				'posts_per_page' => $limit,
				'orderby'        => 'relevance',
				'no_found_rows'  => true,
			)
		);
		return $q->posts;
	}

	/**
	 * Meaningful search terms — lowercased, stop-words and short tokens removed,
	 * so question phrasing ("how do I…") doesn't sink the match.
	 *
	 * @return string[]
	 */
	private function keywords( string $query ): array {
		$stop  = explode(
			' ',
			'how do does did the and for are can you your with this that what where when ' .
			'why which who use using get set will would should please need want help have ' .
			'has from about into'
		);
		$words = preg_split( '/[^a-z0-9]+/', strtolower( $query ) ) ?: array();
		$out   = array();
		foreach ( $words as $word ) {
			if ( strlen( $word ) >= 3 && ! in_array( $word, $stop, true ) ) {
				$out[] = $word;
			}
		}
		return array_values( array_unique( $out ) );
	}

	private function fallback_recent( array $post_types, int $limit ): array {
		$q = new WP_Query(
			array(
				'post_type'      => $post_types,
				'post_status'    => 'publish',
				'posts_per_page' => $limit,
				'orderby'        => 'modified',
				'order'          => 'DESC',
				'no_found_rows'  => true,
			)
		);
		return $q->posts;
	}

	/** All post types we can pull from on this site (before admin exclusions). */
	public function detected_post_types(): array {
		$types = array();
		foreach ( array( 'answyr_kb', 'docs', 'epkb_post_type_1', 'heroic-kb', 'knowledgebase' ) as $candidate ) {
			if ( post_type_exists( $candidate ) ) {
				$types[] = $candidate;
			}
		}
		$types[] = 'post';
		$types[] = 'page';
		return apply_filters( 'answyr_post_types', array_unique( $types ) );
	}

	/** Post types actually used as knowledge sources (detected minus admin exclusions). */
	public function get_active_post_types(): array {
		$excluded = (array) Answyr_Settings::get( 'excluded_post_types', array() );
		return array_values( array_diff( $this->detected_post_types(), $excluded ) );
	}
}
