<?php

namespace WPDRMS\ASP\AI\Service;

use RuntimeException;
use WPDRMS\ASP\AI\AIService;
use WPDRMS\ASP\AI\ORM\AIInteraction;
use WPDRMS\ASP\Index\ORM\IndexTable;
use WPDRMS\ASP\Index\ORM\IndexTableOptions;
use WPDRMS\ASP\Priorities\PrioritiesService;
use WPDRMS\PluginCore\Traits\SingletonTrait;
use WPDRMS\Utils\MB;

if ( ! defined( 'ABSPATH' ) ) {
	die( '-1' );
}

class AIDiagnosticService {
	use SingletonTrait;

	private function __construct() {}

	/**
	 * @param string $phrase
	 * @param int    $asp_id
	 * @param bool   $force
	 * @return array<string, mixed>
	 * @throws RuntimeException When the instance is not found or the AI call fails.
	 */
	public function start( string $phrase, int $asp_id, bool $force = false ): array {
		$instance = wd_asp()->instances->get( $asp_id );
		if ( empty( $instance ) ) {
			throw new RuntimeException( __( 'Search instance not found.', 'ajax-search-pro' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
		}

		$packet    = $this->buildDiagnosticPacket( $phrase, $asp_id, $instance );
		$cache_key = 'asp_ai_diag_' . md5( (string) wp_json_encode( $packet ) );
		$cached    = $force ? false : get_transient( $cache_key );

		if ( is_array( $cached ) && isset( $cached['reply'] ) ) {
			$row                 = new AIInteraction();
			$row->feature        = 'diagnostic';
			$row->prompt_summary = substr( $phrase, 0, 120 );
			$row->tokens_used    = 0;
			$row->cached         = 1;
			$row->save();

			return array(
				'conversation_id' => null,
				'reply'           => $cached['reply'],
				'tokens_used'     => 0,
				'cached'          => true,
				'packet'          => $packet,
			);
		}

		$conversation_id = AIService::instance()->startConversation( 'search_diagnostic' );
		$data            = AIService::instance()->sendMessage( 'diagnostic', $conversation_id, (string) wp_json_encode( $packet ) );

		set_transient( $cache_key, array( 'reply' => $data['reply'] ), DAY_IN_SECONDS );

		return array(
			'conversation_id' => $conversation_id,
			'reply'           => $data['reply'],
			'tokens_used'     => $data['tokens_used'],
			'cached'          => false,
			'packet'          => $packet,
		);
	}

	/**
	 * @param string $conversation_id
	 * @param string $message
	 * @return array{reply: string, tokens_used: int}
	 */
	public function sendMessage( string $conversation_id, string $message ): array {
		$data = AIService::instance()->sendMessage( 'diagnostic', $conversation_id, $message );
		return array(
			'reply'       => $data['reply'],
			'tokens_used' => $data['tokens_used'],
		);
	}

	/**
	 * Assembles the diagnosis packet from real plugin data so the AI never hallucinates.
	 *
	 * @param string $phrase
	 * @param int    $asp_id
	 * @param mixed  $instance
	 * @return array<string, mixed>
	 */
	private function buildDiagnosticPacket( string $phrase, int $asp_id, $instance ): array {
		$data            = is_array( $instance['data'] ) ? $instance['data'] : array();
		$min_word_length = max( 1, (int) ( $data['min_word_length'] ?? 3 ) );
		$it_opts         = IndexTableOptions::instance();
		$stopwords       = $it_opts->use_stopwords->value ? (array) $it_opts->stopwords->value : array();

		$token_analysis = $this->buildTokenAnalysis( $phrase, $min_word_length, $stopwords );

		return array_merge(
			array(
				'phrase'             => $phrase,
				'asp_id'             => $asp_id,
				'instance_name'      => (string) ( $instance['name'] ?? '' ),
				'enabled_post_types' => is_array( $data['customtypes'] ?? null ) ? $data['customtypes'] : array(),
				'keyword_logic'      => (string) ( $data['keyword_logic'] ?? 'and' ),
				'min_word_length'    => $min_word_length,
				'stopwords_enabled'  => (bool) $it_opts->use_stopwords->value,
			),
			$token_analysis,
			$this->buildIndexHits( $token_analysis['phrase_tokens'], $phrase ),
			$this->buildPriorityMatches( $phrase, $asp_id ),
			$this->buildTermFilters( $data ),
			$this->buildConfigSummary( $data, $it_opts )
		);
	}

	/**
	 * Splits the phrase into tokens and classifies each as stopped, too-short, or searchable.
	 *
	 * @param string   $phrase
	 * @param int      $min_word_length
	 * @param string[] $stopwords
	 * @return array{phrase_tokens: string[], stopped_tokens: string[], short_tokens_dropped: string[]}
	 */
	private function buildTokenAnalysis( string $phrase, int $min_word_length, array $stopwords ): array {
		$raw_tokens = array_filter(
			explode( ' ', strtolower( trim( $phrase ) ) ),
			static function ( $t ) {
				return $t !== '';
			}
		);

		$phrase_tokens  = array();
		$stopped_tokens = array();
		$short_tokens   = array();

		foreach ( $raw_tokens as $tok ) {
			if ( MB::strlen( $tok ) < $min_word_length ) {
				$short_tokens[] = $tok;
			} elseif ( in_array( $tok, $stopwords, true ) ) {
				$stopped_tokens[] = $tok;
			} else {
				$phrase_tokens[] = $tok;
			}
		}

		return array(
			'phrase_tokens'        => $phrase_tokens,
			'stopped_tokens'       => $stopped_tokens,
			'short_tokens_dropped' => $short_tokens,
		);
	}

	/**
	 * Queries the index table for each searchable token and the full phrase.
	 *
	 * @param string[] $phrase_tokens
	 * @param string   $phrase
	 * @return array{index_hits: array}
	 */
	private function buildIndexHits( array $phrase_tokens, string $phrase ): array {
		global $wpdb;

		$table      = IndexTable::getTableName();
		$blog_id    = (int) get_current_blog_id();
		$index_hits = array();

		foreach ( array_unique( array_merge( array( strtolower( $phrase ) ), $phrase_tokens ) ) as $tok ) {
			if ( $tok === '' ) {
				continue;
			}
			$like = $wpdb->esc_like( $tok ) . '%';
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
			$rows = $wpdb->get_results(
				$wpdb->prepare(
					"SELECT post_type, SUM(content + title + comment + tag + link + author + excerpt + customfield) AS occurrences FROM `{$table}` WHERE term LIKE %s AND blogid = %d GROUP BY post_type ORDER BY occurrences DESC LIMIT 10", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
					$like,
					$blog_id
				),
				ARRAY_A
			);

			if ( ! empty( $rows ) ) {
				$post_types   = array_column( is_array( $rows ) ? $rows : array(), 'post_type' );
				$total        = (int) array_sum( array_column( is_array( $rows ) ? $rows : array(), 'occurrences' ) );
				$index_hits[] = array(
					'term'              => $tok,
					'post_types'        => $post_types,
					'total_occurrences' => $total,
				);
			}
		}

		return array( 'index_hits' => $index_hits );
	}

	/**
	 * Finds all enabled priority groups that match the phrase for the given instance.
	 *
	 * @param string $phrase
	 * @param int    $asp_id
	 * @return array{matching_priority_groups: array}
	 */
	private function buildPriorityMatches( string $phrase, int $asp_id ): array {
		$all_groups          = PrioritiesService::instance()->getSorted( 'priority', 'desc' );
		$matching_priorities = array();

		foreach ( $all_groups as $group ) {
			if ( ! $group['enabled'] ) {
				continue;
			}
			$group_instance = (int) ( $group['instance'] ?? 0 );
			if ( $group_instance !== 0 && $group_instance !== $asp_id ) {
				continue;
			}
			$group_phrase = (string) ( $group['phrase'] ?? '' );
			$logic        = (string) ( $group['phrase_logic'] ?? 'disabled' );

			$matches = false;
			switch ( $logic ) {
				case 'disabled':
					$matches = true;
					break;
				case 'any':
					$matches = stripos( $phrase, $group_phrase ) !== false;
					break;
				case 'exact':
					$matches = strcasecmp( $phrase, $group_phrase ) === 0;
					break;
				case 'start':
					$matches = strncasecmp( $phrase, $group_phrase, strlen( $group_phrase ) ) === 0;
					break;
				case 'end':
					$matches = substr_compare( strtolower( $phrase ), strtolower( $group_phrase ), -strlen( $group_phrase ) ) === 0;
					break;
			}

			if ( $matches ) {
				$matching_priorities[] = array(
					'name'         => (string) ( $group['name'] ?? '' ),
					'phrase_logic' => $logic,
					'priority'     => (int) ( $group['priority'] ?? 100 ),
				);
			}
		}

		return array( 'matching_priority_groups' => $matching_priorities );
	}

	/**
	 * Extracts include/exclude taxonomy term filters and post exclusion counts.
	 *
	 * @param array<string, mixed> $data  Instance data array.
	 * @return array{include_terms: array, exclude_terms: array, excluded_post_count: int, include_cpt_ids_count: int, exclude_cpt_ids_count: int}
	 */
	private function buildTermFilters( array $data ): array {
		$include_terms = array();
		$ib_data       = is_array( $data['include_by_terms'] ?? null ) ? $data['include_by_terms'] : array();
		foreach ( (array) ( $ib_data['terms'] ?? array() ) as $entry ) {
			if ( ! is_array( $entry ) ) {
				continue;
			}
			$term_obj        = get_term( (int) ( $entry['id'] ?? 0 ) );
			$include_terms[] = array(
				'taxonomy' => (string) ( $entry['taxonomy'] ?? '' ),
				'term_id'  => (int) ( $entry['id'] ?? 0 ),
				'name'     => ( $term_obj instanceof \WP_Term ) ? $term_obj->name : '',
			);
		}

		$exclude_terms = array();
		$eb_data       = is_array( $data['exclude_by_terms'] ?? null ) ? $data['exclude_by_terms'] : array();
		foreach ( (array) ( $eb_data['terms'] ?? array() ) as $entry ) {
			if ( ! is_array( $entry ) ) {
				continue;
			}
			$term_obj        = get_term( (int) ( $entry['id'] ?? 0 ) );
			$exclude_terms[] = array(
				'taxonomy' => (string) ( $entry['taxonomy'] ?? '' ),
				'term_id'  => (int) ( $entry['id'] ?? 0 ),
				'name'     => ( $term_obj instanceof \WP_Term ) ? $term_obj->name : '',
			);
		}

		$excluded_str        = str_replace( ' ', '', (string) ( $data['excludeposts'] ?? '' ) );
		$excluded_post_count = $excluded_str !== '' ? count( array_filter( explode( ',', $excluded_str ) ) ) : 0;

		$inc_cpt = is_array( $data['include_cpt'] ?? null ) ? $data['include_cpt'] : array();
		$exc_cpt = is_array( $data['exclude_cpt'] ?? null ) ? $data['exclude_cpt'] : array();

		return array(
			'include_terms'         => $include_terms,
			'exclude_terms'         => $exclude_terms,
			'excluded_post_count'   => $excluded_post_count,
			'include_cpt_ids_count' => ( ! empty( $inc_cpt['ids'] ) && is_array( $inc_cpt['ids'] ) ) ? count( $inc_cpt['ids'] ) : 0,
			'exclude_cpt_ids_count' => ( ! empty( $exc_cpt['ids'] ) && is_array( $exc_cpt['ids'] ) ) ? count( $exc_cpt['ids'] ) : 0,
		);
	}

	/**
	 * Extracts search-behaviour config from the instance data.
	 *
	 * @param array<string, mixed> $data     Instance data array.
	 * @param IndexTableOptions    $it_opts
	 * @return array<string, mixed>
	 */
	private function buildConfigSummary( array $data, IndexTableOptions $it_opts ): array {
		$exact_match_only = (bool) ( $data['exactonly'] ?? false );
		$exact_match_loc  = (string) ( $data['exact_match_location'] ?? 'anywhere' );
		$exact_secondary  = (bool) ( $data['exact_m_secondary'] ?? false );

		$cf_items = (string) ( $data['custom_field_items'] ?? '' );
		$has_cf   = $cf_items !== '' && $cf_items !== '[]' && $cf_items !== '{}';

		$user_filter = null;
		$ucb_data    = is_array( $data['exclude_content_by_users'] ?? null ) ? $data['exclude_content_by_users'] : array();
		if ( ! empty( $ucb_data['users'] ) && is_array( $ucb_data['users'] ) ) {
			$user_filter = array(
				'op_type'    => (string) ( $ucb_data['op_type'] ?? 'exclude' ),
				'user_count' => count( $ucb_data['users'] ),
			);
		}

		return array(
			'post_statuses'                  => array_values(
				array_filter( array_map( 'trim', explode( ',', (string) ( $data['post_status'] ?? 'publish' ) ) ) )
			),
			'include_password_protected'     => (bool) ( $data['post_password_protected'] ?? false ),
			'date_filter_enabled'            => (bool) ( $data['exclude_dates_on'] ?? false ),
			'exact_match_only'               => $exact_match_only,
			'exact_match_location'           => $exact_match_only ? $exact_match_loc : null,
			'exact_match_secondary'          => $exact_match_only ? $exact_secondary : null,
			'user_content_filter'            => $user_filter,
			'has_custom_field_filters'       => $has_cf,
			'custom_field_logic'             => $has_cf ? (string) ( $data['cf_logic'] ?? 'AND' ) : null,
			'cf_exclusion_rules_count'       => count( $it_opts->cf_exclusion_rules->rules ),
			'taxonomy_exclusion_rules_count' => count( $it_opts->taxonomy_exclusion_rules->rules ),
		);
	}
}
