<?php
/**
 * Search Content Tool.
 *
 * Provides search capabilities for WordPress content. Supports lexical
 * (keyword) and semantic (embeddings-based) search modes.
 *
 * @package   Agenistrator
 * @since     1.0.0
 * @author    Alex Ruban <alex@maximum.guru>
 * @author    Maximum Software Inc <contact@maximum.software>
 * @license   GPL-3.0-only
 * @link      https://agenistrator.com/
 *
 * SPDX-FileCopyrightText: 2025-2026 Alex Ruban <alex@maximum.guru>
 * SPDX-FileCopyrightText: 2025-2026 Maximum Software Inc <contact@maximum.software>
 * SPDX-License-Identifier: GPL-3.0-only
 */

namespace Agenistrator;

// Direct access prevention
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Search_Content_Tool class.
 */
class Search_Content_Tool extends Tool {

	/**
	 * Get the tool ID.
	 *
	 * @return string The tool ID.
	 * @since     1.0.0
	 */
	public function get_id() {
		return 'search_content';
	}

	/**
	 * Get the tool name.
	 *
	 * @return string The tool name.
	 * @since     1.0.0
	 */
	public function get_name() {
		return __( 'Search Content', 'agenistrator' );
	}

	/**
	 * Get the description of the tool.
	 *
	 * @return string The description of the tool.
	 * @since     1.0.0
	 */
	public function get_description() {
		$desc = __( 'Search for WordPress content including posts and pages.', 'agenistrator' );
		if ( $this->is_semantic_available() ) {
			$desc .= ' ' . __( 'Supports semantic (meaning-based) and lexical (keyword-based) search.', 'agenistrator' );
		}
		return $desc;
	}

	/**
	 * Get the parameters for this tool.
	 *
	 * @return array Parameters schema in JSON Schema format.
	 * @since     1.0.0
	 */
	public function get_parameters() {
		$properties = array(
			'query' => array(
				'type'        => 'string',
				'description' => __( 'The search query string', 'agenistrator' ),
			),
			'limit' => array(
				'type'        => 'integer',
				'description' => __( 'Maximum number of results to return. Default is 5.', 'agenistrator' ),
			),
		);
		$required   = array( 'query' );

		// Add type parameter only if embeddings are configured
		if ( $this->is_semantic_available() ) {
			$properties['type'] = array(
				'type'        => 'string',
				'enum'        => array( 'semantic', 'lexical' ),
				'description' => __( 'Search type: semantic (meaning-based via embeddings) or lexical (keyword-based). Always use semantic search type unless exact keyword match is required.', 'agenistrator' ),
			);
		}

		return array(
			'type'       => 'object',
			'properties' => $properties,
			'required'   => $required,
		);
	}

	/**
	 * Get the default enabled status for this tool.
	 *
	 * @return bool True if enabled by default, false otherwise.
	 * @since     1.0.0
	 */
	public function get_default_enabled_status() {
		return true;
	}

	/**
	 * Check if the current user has permission to execute this tool.
	 *
	 * @return bool Whether the user has permission.
	 * @since     1.0.0
	 */
	public function current_user_can_execute() {
		return current_user_can( 'read' );
	}

	/**
	 * Check if this tool is read-only.
	 *
	 * @return bool True - this tool only retrieves data.
	 * @since     1.0.0
	 */
	public function is_read_only() {
		return true;
	}

	/**
	 * Get usage instructions for the LLM
	 *
	 * @return string Detailed instructions for using lexical and semantic search.
	 * @since     1.0.0
	 */
	public function get_instructions() {
		$instructions       = '';
		$semantic_available = $this->is_semantic_available();

		if ( $semantic_available ) {
			$instructions .= "SEMANTIC SEARCH (meaning-based, RECOMMENDED):
- Finds content by conceptual similarity, not exact keywords
- Query is converted to a vector embedding and matched against content embeddings
- Effective for: finding related topics, synonyms, conceptual matches
- Prefer semantic search when you're unsure of exact terminology used in content
- Returns matching content chunks showing which parts matched your query

";
		}

		$instructions .= 'LEXICAL SEARCH (keyword-based):
- Uses WordPress default search which splits your query into individual words
- ALL words must appear somewhere in the content (implicit AND logic)
- Words do NOT need to appear together or in order
- Searches post titles and content only
- DOES NOT support: OR, AND, NOT operators, quoted phrases, wildcards, or regex
- Any operators you include are treated as literal text to search for';

		if ( $semantic_available ) {
			$instructions .= '

WHEN TO USE EACH:
- Use SEMANTIC (default): When searching for concepts, topics, or when unsure of exact wording
- Use LEXICAL: Only when you need exact keyword matches or know specific terms exist';
		}

		$instructions .= "\n\n" . 'EXCERPT FIELDS: `has_excerpt` indicates if a manual excerpt is set. If true, `excerpt` contains the stored value. If false, `auto_excerpt` contains a truncated auto-generated excerpt from content (not stored - do not use for updates).';

		return $instructions;
	}

	/**
	 * Check if semantic search is available.
	 *
	 * @return bool True if semantic search can be performed.
	 */
	private function is_semantic_available() {
		return Embeddings_Manager::get_instance()->is_search_operational();
	}

	/**
	 * Execute the search tool.
	 *
	 * @param array $args Validated arguments for the tool.
	 * @return array Search results.
	 * @since     1.0.0
	 */
	protected function execute( $args ) {
		$limit              = min( max( intval( $args['limit'] ?? 5 ), 1 ), 50 );
		$semantic_available = $this->is_semantic_available();
		$type               = $args['type'] ?? ( $semantic_available ? 'semantic' : 'lexical' );

		// Validate type argument
		$types = array( 'lexical' );
		if ( $semantic_available ) {
			array_unshift( $types, 'semantic' );
		}
		if ( ! in_array( $type, $types, true ) ) {
			return $this->error_response(
				String_Utils::replace_tags(
					__( 'Invalid search type. Supported types are: {types}.', 'agenistrator' ),
					array( 'types' => implode( __( ', ', 'agenistrator' ), $types ) )
				)
			);
		}
		if ( 'semantic' === $type && ! $semantic_available ) {
			return $this->error_response( __( 'Semantic search is not available. Please configure an embeddings model in settings.', 'agenistrator' ) );
		}

		try {
			if ( 'semantic' === $type && $semantic_available ) {
				$results = $this->execute_semantic_search( $args['query'], $limit );
			} else {
				$results = $this->execute_lexical_search( $args['query'], $limit );
			}

			return array(
				'success' => true,
				'query'   => $args['query'],
				'type'    => $type,
				'count'   => count( $results ),
				'results' => $results,
			);
		} catch ( \Throwable $e ) {
			return $this->error_response(
				String_Utils::replace_tags(
					__( 'Search failed: {error}', 'agenistrator' ),
					array( 'error' => $e->getMessage() )
				)
			);
		}
	}

	/**
	 * Execute lexical (keyword) search using WP_Query.
	 *
	 * @param string $query The search query string.
	 * @param int    $limit Maximum number of results.
	 * @return array Array of formatted post results.
	 */
	private function execute_lexical_search( $query, $limit ) {
		$query_args   = array(
			's'              => $query,
			'post_type'      => array( 'post', 'page' ),
			'posts_per_page' => $limit,
		);
		$search_query = new \WP_Query( $query_args );

		$results = array();
		if ( $search_query->have_posts() ) {
			while ( $search_query->have_posts() ) {
				$search_query->the_post();
				$results[] = $this->format_post_result( get_post() );
			}
			wp_reset_postdata();
		}
		return $results;
	}

	/**
	 * Execute semantic (embeddings-based) search.
	 *
	 * @param string $query The search query string.
	 * @param int    $limit Maximum number of results.
	 * @return array Array of formatted post results with matching chunks.
	 */
	private function execute_semantic_search( $query, $limit ) {
		$embeddings_manager = Embeddings_Manager::get_instance();
		$raw_results        = $embeddings_manager->search_similar_content( $query, $limit );

		$results = array();
		foreach ( $raw_results as $item ) {
			if ( 'post' !== $item['content_type'] ) {
				continue;
			}

			$post = get_post( $item['content_id'] );
			if ( ! $post ) {
				continue;
			}

			$result = $this->format_post_result( $post );

			// Reconstruct matching chunks from metadata
			if ( ! empty( $item['chunks'] ) ) {
				$matching_chunks = array();
				foreach ( $item['chunks'] as $chunk_meta ) {
					$chunk_content = $embeddings_manager->get_chunk( $post, $chunk_meta );
					if ( null !== $chunk_content ) {
						$matching_chunks[] = $chunk_content;
					}
				}
				if ( ! empty( $matching_chunks ) ) {
					$result['matching_chunks'] = $matching_chunks;
				}
			}

			$results[] = $result;
		}
		return $results;
	}

	/**
	 * Format a post into a consistent result structure.
	 *
	 * @param \WP_Post $post The post to format.
	 * @return array Formatted post data.
	 */
	private function format_post_result( $post ) {
		$has_excerpt = has_excerpt( $post );
		$result      = array(
			'id'          => $post->ID,
			'title'       => $post->post_title,
			'slug'        => $post->post_name,
			'url'         => get_permalink( $post ),
			'has_excerpt' => $has_excerpt,
			'post_type'   => $post->post_type,
			'status'      => $post->post_status,
			'date'        => $post->post_date,
			'modified'    => $post->post_modified,
			'author'      => array(
				'id'   => $post->post_author,
				'name' => get_the_author_meta( 'display_name', $post->post_author ),
			),
			'categories'  => array(),
			'tags'        => array(),
		);

		if ( $has_excerpt ) {
			$result['excerpt'] = $post->post_excerpt;
		} else {
			$auto_excerpt = html_entity_decode( wp_strip_all_tags( get_the_excerpt( $post ) ), ENT_QUOTES, 'UTF-8' );
			if ( ! empty( $auto_excerpt ) ) {
				$result['auto_excerpt'] = String_Utils::truncate_to_token_boundary_with_ellipsis( $auto_excerpt, 200 );
			}
		}

		$categories = get_the_category( $post->ID );
		if ( $categories ) {
			foreach ( $categories as $category ) {
				$result['categories'][] = array(
					'id'   => $category->term_id,
					'name' => $category->name,
					'slug' => $category->slug,
				);
			}
		}

		$tags = get_the_tags( $post->ID );
		if ( $tags ) {
			foreach ( $tags as $tag ) {
				$result['tags'][] = array(
					'id'   => $tag->term_id,
					'name' => $tag->name,
					'slug' => $tag->slug,
				);
			}
		}

		if ( has_post_thumbnail( $post->ID ) ) {
			$result['featured_image'] = array(
				'id'  => get_post_thumbnail_id( $post->ID ),
				'url' => get_the_post_thumbnail_url( $post->ID, 'full' ),
				'alt' => get_post_meta( get_post_thumbnail_id( $post->ID ), '_wp_attachment_image_alt', true ),
			);
		}

		return $result;
	}
}
