<?php

namespace WPDRMS\ASP\AI\Service;

use WPDRMS\ASP\AI\AIService;
use WPDRMS\ASP\AI\ORM\AIOptions;
use WPDRMS\ASP\Index\ORM\IndexTable;
use WPDRMS\ASP\Statistics\ORM\Search as StatSearch;
use WPDRMS\ASP\Synonyms\SynonymsService;
use WPDRMS\PluginCore\Traits\SingletonTrait;
use WPDRMS\Utils\MB;

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

class AISynonymService {
	use SingletonTrait;

	private function __construct() {}

	/**
	 * @param string $phrase  already lowercased + sanitized
	 * @param string $lang    multilingual language code; '' = site default / no language filter
	 * @return array{suggestions: array}
	 */
	public function discover( string $phrase, string $lang = '' ): array {
		global $wpdb;

		$index_table = IndexTable::getTableName();
		$stat_table  = StatSearch::getTableName();

		// Top 30 most-frequent site terms — site vocabulary context only, not a constraint.
		[ $idx_frag, $idx_args ] = $this->langFilter( $lang, 'index' );
		$idx_where               = $idx_frag !== '' ? " WHERE {$idx_frag}" : '';
		$top_sql                 = "SELECT term, SUM(content + title + comment + tag + link + author + excerpt + customfield) AS occurrences FROM `{$index_table}`{$idx_where} GROUP BY term ORDER BY occurrences DESC LIMIT 30"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$top_terms               = $this->query( $top_sql, $idx_args );

		// Top 20 successful search phrases — domain/tone context.
		[ $stat_frag, $stat_args ] = $this->langFilter( $lang, 'stat' );
		$stat_and                  = $stat_frag !== '' ? " AND {$stat_frag}" : '';
		$phrases_sql               = "SELECT phrase, COUNT(*) AS cnt FROM `{$stat_table}` WHERE phrase != '' AND found_results > 0{$stat_and} GROUP BY phrase ORDER BY cnt DESC LIMIT 20"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$popular_phrases           = $this->query( $phrases_sql, $stat_args );

		$context_terms   = ! empty( $top_terms ) ? (string) wp_json_encode( $top_terms ) : 'none';
		$context_phrases = ! empty( $popular_phrases ) ? (string) wp_json_encode( $popular_phrases ) : 'none';
		$language_label  = $this->promptLanguage( $lang );

		$prompt = "language: \"{$language_label}\"\n\nkeyword: \"{$phrase}\"\n\nindex_vocabulary (reference only):\n{$context_terms}\n\npopular_searches (domain context only):\n{$context_phrases}";

		$data    = AIService::instance()->generate( 'synonym_discover', 'synonym_discover', $prompt, 86400 );
		$raw     = AIService::extractJsonPayload( (string) ( $data['result'] ?? '' ) );
		$decoded = json_decode( $raw, true );

		return array( 'suggestions' => is_array( $decoded ) ? $decoded : array() );
	}

	/**
	 * Generates synonyms for the top-N most popular indexed keywords in one batched gateway call.
	 *
	 * Returns one entry per keyword with each generated synonym flagged when it is already saved,
	 * and a flag when the keyword already has synonyms (so the UI can mark them — and so keywords
	 * with existing synonyms are kept, since the AI may add complementary ones). Results are NOT
	 * persisted; they are returned to the review UI which then calls applyBulk() with the accepted set.
	 *
	 * @param int    $count  number of top keywords to generate for (clamped 5–100)
	 * @param string $lang   multilingual language code; '' = site default / no language filter
	 * @return array{items: array<int, array<string, mixed>>, lang: string, count: int}
	 */
	public function generateBulk( int $count, string $lang = '' ): array {
		global $wpdb;

		$count       = max( 5, min( 100, $count ) );
		$index_table = IndexTable::getTableName();
		$stat_table  = StatSearch::getTableName();
		$blog_id     = (int) get_current_blog_id();

		// Top-N popular index terms (weighted occurrence), language-filtered.
		[ $idx_frag, $idx_args ] = $this->langFilter( $lang, 'index' );
		$idx_and                 = $idx_frag !== '' ? " AND {$idx_frag}" : '';
		$kw_sql                  = "SELECT term, SUM(content + title + comment + tag + link + author + excerpt + customfield) AS occ FROM `{$index_table}` WHERE blogid = %d AND LENGTH(term) > 2{$idx_and} GROUP BY term ORDER BY occ DESC LIMIT %d"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$kw_args                 = array_merge( array( $blog_id ), $idx_args, array( $count ) );
		$rows                    = $this->query( $kw_sql, $kw_args );

		$keywords = array();
		foreach ( $rows as $row ) {
			$kw = trim( (string) ( $row['term'] ?? '' ) );
			if ( $kw !== '' ) {
				$keywords[] = $kw;
			}
		}

		if ( count( $keywords ) === 0 ) {
			return array(
				'items' => array(),
				'lang'  => $lang,
				'count' => 0,
			);
		}

		// Popular successful phrases — domain context for the agent.
		[ $stat_frag, $stat_args ] = $this->langFilter( $lang, 'stat' );
		$stat_and                  = $stat_frag !== '' ? " AND {$stat_frag}" : '';
		$phrases_sql               = "SELECT phrase, COUNT(*) AS cnt FROM `{$stat_table}` WHERE phrase != '' AND found_results > 0{$stat_and} GROUP BY phrase ORDER BY cnt DESC LIMIT 20"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$popular_phrases           = $this->query( $phrases_sql, $stat_args );

		$language_label = $this->promptLanguage( $lang );
		$prompt         = (string) wp_json_encode(
			array(
				'language'         => $language_label,
				'keywords'         => $keywords,
				'popular_searches' => $popular_phrases,
			)
		);

		$data    = AIService::instance()->generate( 'synonym_generator', 'synonym_generator', $prompt, 86400 );
		$raw     = AIService::extractJsonPayload( (string) ( $data['result'] ?? '' ) );
		$decoded = json_decode( $raw, true );
		$map     = is_array( $decoded ) ? $decoded : array();

		$service = SynonymsService::instance();
		$items   = array();

		foreach ( $keywords as $kw ) {
			$generated = isset( $map[ $kw ] ) && is_array( $map[ $kw ] ) ? $map[ $kw ] : array();

			$existing     = $service->get( $kw, $lang );
			$existing_arr = is_array( $existing ) ? $existing : array();
			$existing_lc  = array_map(
				static function ( $s ) {
					return MB::strtolower( (string) $s );
				},
				$existing_arr
			);

			$kw_lc    = MB::strtolower( $kw );
			$synonyms = array();
			$seen     = array();

			foreach ( $generated as $g ) {
				$term = trim( (string) $g );
				if ( $term === '' ) {
					continue;
				}
				$term_lc = MB::strtolower( $term );
				if ( $term_lc === $kw_lc || isset( $seen[ $term_lc ] ) ) {
					continue;
				}
				$seen[ $term_lc ] = true;

				$synonyms[] = array(
					'term'   => $term,
					'exists' => in_array( $term_lc, $existing_lc, true ),
				);
			}

			if ( count( $synonyms ) === 0 ) {
				continue;
			}

			$items[] = array(
				'keyword'              => $kw,
				'keyword_has_existing' => count( $existing_arr ) > 0,
				'synonyms'             => $synonyms,
			);
		}

		return array(
			'items' => $items,
			'lang'  => $lang,
			'count' => count( $items ),
		);
	}

	/**
	 * Applies an accepted set of generated synonyms in bulk.
	 *
	 * @param array<int, array<string, mixed>> $items  each: { keyword: string, synonyms: string[] }
	 * @param string                           $lang
	 * @return array{applied: int}
	 */
	public function applyBulk( array $items, string $lang = '' ): array {
		$service = SynonymsService::instance();
		$applied = 0;

		foreach ( $items as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}

			$keyword  = sanitize_text_field( (string) ( $item['keyword'] ?? '' ) );
			$synonyms = isset( $item['synonyms'] ) && is_array( $item['synonyms'] ) ? $item['synonyms'] : array();
			$synonyms = array_values(
				array_filter(
					array_map(
						static function ( $s ) {
							return sanitize_text_field( (string) $s );
						},
						$synonyms
					),
					static function ( $s ) {
						return $s !== '';
					}
				)
			);

			if ( $keyword === '' || count( $synonyms ) === 0 ) {
				continue;
			}

			$existing = $service->get( $keyword, $lang );
			if ( $existing === false || ! is_array( $existing ) ) {
				$service->add( $keyword, $synonyms, $lang );
			} else {
				$service->update( $keyword, array_unique( array_merge( $existing, $synonyms ) ), $lang, true );
			}
			++$applied;
		}

		if ( $applied > 0 ) {
			update_option( 'asp_synonyms_index_stale', 1 );
		}

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

	/**
	 * @param string $keyword
	 * @param string $synonym
	 * @param string $lang
	 * @return array{success: true}
	 */
	public function apply( string $keyword, string $synonym, string $lang = '' ): array {
		$service  = SynonymsService::instance();
		$existing = $service->get( $keyword, $lang );

		if ( $existing === false || ! is_array( $existing ) ) {
			$service->add( $keyword, array( $synonym ), $lang );
		} else {
			$service->update( $keyword, array_unique( array_merge( $existing, array( $synonym ) ) ), $lang, true );
		}

		update_option( 'asp_synonyms_index_stale', 1 );

		return array( 'success' => true );
	}

	/**
	 * Resolves the language label sent to the gateway prompt (the language the AI writes synonyms in).
	 *
	 * An explicit multilingual code is used as-is. For the default storage language ('') the admin's
	 * "Content language" setting (Search AI settings) is used; when that is unset we fall back to
	 * "site default", which tells the gateway to infer the language from the keyword/context.
	 * This only affects the generation language — synonym storage still uses $storageLang unchanged.
	 *
	 * @param string $storage_lang
	 * @return string
	 */
	private function promptLanguage( string $storage_lang ): string {
		if ( $storage_lang !== '' ) {
			return $storage_lang;
		}
		$content_language = (string) AIOptions::instance()->content_language->value;
		return $content_language !== '' ? $content_language : 'site default';
	}

	/**
	 * Builds a language filter fragment + bind args. A requested $lang of '' means
	 * "site default / any" → no filter (preserves the pre-language behaviour).
	 *
	 * The tables disagree on their default-language sentinel: the index uses '0', the
	 * statistics table uses the language code (NULL/'' for none), so the fragment differs.
	 *
	 * @param string $lang
	 * @param string $mode  'index' | 'stat'
	 * @return array{0: string, 1: array<int, string>}  [sql_fragment, bind_args]
	 */
	private function langFilter( string $lang, string $mode ): array {
		if ( $lang === '' ) {
			return array( '', array() );
		}
		if ( $mode === 'index' ) {
			return array( "(lang = %s OR lang = '0')", array( $lang ) );
		}
		return array( 'lang = %s', array( $lang ) );
	}

	/**
	 * Runs a SELECT, preparing it only when bind args are present.
	 *
	 * @param string             $sql
	 * @param array<int, string> $args
	 * @return array<int, array<string, mixed>>
	 */
	private function query( string $sql, array $args ): array {
		global $wpdb;
		if ( count( $args ) > 0 ) {
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
			$rows = $wpdb->get_results( $wpdb->prepare( $sql, $args ), ARRAY_A );
		} else {
			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching
			$rows = $wpdb->get_results( $sql, ARRAY_A );
		}
		return is_array( $rows ) ? $rows : array();
	}
}
