<?php
/**
 * Chat service: settings, WordPress AI Client, RAG search, answers.
 *
 * @package Maca_Tjatt
 */

defined( 'ABSPATH' ) || exit;

/**
 * Class Maca_Tjatt_Service
 */
class Maca_Tjatt_Service {

	const OPTION_PREFIX = 'maca_tjatt_';

	/**
	 * @return bool
	 */
	public static function is_enabled() {
		return '1' === (string) get_option( self::OPTION_PREFIX . 'enabled', '0' );
	}

	/**
	 * @return bool
	 */
	public static function is_ai_available() {
		return Maca_Tjatt_AI_Client::is_available();
	}

	/**
	 * Backward-compatible alias.
	 *
	 * @return bool
	 */
	public static function has_api_key() {
		return self::is_ai_available();
	}

	/**
	 * @return string
	 */
	public static function get_display_name() {
		$name = trim( (string) get_option( self::OPTION_PREFIX . 'name', '' ) );

		if ( '' !== $name ) {
			return $name;
		}

		return __( 'Website assistant', 'maca-tjatt' );
	}

	/**
	 * @return array<string, string>
	 */
	public static function get_guest_ui_strings() {
		return array(
			'chatLabel'    => __( 'Chat', 'maca-tjatt' ),
			'placeholder'  => __( 'Ask a question about the website…', 'maca-tjatt' ),
			'send'         => __( 'Send', 'maca-tjatt' ),
			'close'        => __( 'Close', 'maca-tjatt' ),
			'thinking'     => __( 'Thinking…', 'maca-tjatt' ),
			'error'        => __( 'Could not get an answer. Please try again.', 'maca-tjatt' ),
			'yourQuestion' => __( 'Your question', 'maca-tjatt' ),
		);
	}

	/**
	 * Parse and sanitize chat history from an AJAX JSON payload.
	 *
	 * @param mixed $raw Raw history value.
	 * @return array<int, array<string, string>>
	 */
	public static function parse_request_history( $raw ) {
		if ( ! is_string( $raw ) ) {
			return array();
		}

		$decoded = json_decode( sanitize_text_field( wp_unslash( $raw ) ), true );

		if ( ! is_array( $decoded ) ) {
			return array();
		}

		$history = array();

		foreach ( $decoded as $turn ) {
			if ( ! is_array( $turn ) ) {
				continue;
			}

			$role    = isset( $turn['role'] ) ? sanitize_key( (string) $turn['role'] ) : '';
			$content = isset( $turn['content'] ) ? sanitize_text_field( (string) $turn['content'] ) : '';

			if ( ! in_array( $role, array( 'user', 'assistant' ), true ) || '' === $content ) {
				continue;
			}

			$history[] = array(
				'role'    => $role,
				'content' => $content,
			);
		}

		return $history;
	}

	/**
	 * @return string
	 */
	public static function get_default_avatar_url() {
		return plugins_url( 'assets/images/default-avatar.svg', MACA_TJATT_PLUGIN_FILE );
	}

	/**
	 * Bundled avatar ids shipped with the plugin.
	 *
	 * @return array<int, string>
	 */
	public static function get_bundled_avatar_ids() {
		return array(
			'avatar_bagare',
			'avatar_blogger',
			'avatar_coder',
			'avatar_doctor',
			'avatar_fotograf',
			'avatar_fysiker',
			'avatar_handyman',
			'avatar_kock',
			'avatar_kreator',
			'avatar_maths',
			'avatar_sales',
			'avatar_vetrinar',
			'avatar_vvs',
			'avatar_youtuber',
		);
	}

	/**
	 * @return array<string, string>
	 */
	private static function get_bundled_avatar_labels() {
		return array(
			'avatar_bagare'   => __( 'Baker', 'maca-tjatt' ),
			'avatar_blogger'  => __( 'Blogger', 'maca-tjatt' ),
			'avatar_coder'    => __( 'Coder', 'maca-tjatt' ),
			'avatar_doctor'   => __( 'Doctor', 'maca-tjatt' ),
			'avatar_fotograf' => __( 'Photographer', 'maca-tjatt' ),
			'avatar_fysiker'  => __( 'Physicist', 'maca-tjatt' ),
			'avatar_handyman' => __( 'Handyman', 'maca-tjatt' ),
			'avatar_kock'     => __( 'Chef', 'maca-tjatt' ),
			'avatar_kreator'  => __( 'Creator', 'maca-tjatt' ),
			'avatar_maths'    => __( 'Mathematician', 'maca-tjatt' ),
			'avatar_sales'    => __( 'Sales', 'maca-tjatt' ),
			'avatar_vetrinar' => __( 'Veterinarian', 'maca-tjatt' ),
			'avatar_vvs'      => __( 'Plumber', 'maca-tjatt' ),
			'avatar_youtuber' => __( 'YouTuber', 'maca-tjatt' ),
		);
	}

	/**
	 * Resolve a bundled avatar URL from its id.
	 *
	 * @param string $id Avatar id.
	 * @return string
	 */
	public static function get_bundled_avatar_url( $id ) {
		$id = sanitize_key( (string) $id );

		if ( '' === $id || ! in_array( $id, self::get_bundled_avatar_ids(), true ) ) {
			return '';
		}

		return plugins_url( 'assets/avatars/' . $id . '.png', MACA_TJATT_PLUGIN_FILE );
	}

	/**
	 * Bundled avatar files shipped with the plugin.
	 *
	 * @return array<string, array{url: string, label: string}>
	 */
	public static function get_bundled_avatars() {
		static $avatars = null;

		if ( null !== $avatars ) {
			return $avatars;
		}

		$avatars = array();
		$labels  = self::get_bundled_avatar_labels();

		foreach ( self::get_bundled_avatar_ids() as $id ) {
			$file = $id . '.png';
			$path = MACA_TJATT_PLUGIN_DIR . 'assets/avatars/' . $file;

			if ( ! is_readable( $path ) ) {
				continue;
			}

			$avatars[ $id ] = array(
				'url'   => plugins_url( 'assets/avatars/' . $file, MACA_TJATT_PLUGIN_FILE ),
				'label' => isset( $labels[ $id ] ) ? $labels[ $id ] : ucwords( str_replace( '_', ' ', preg_replace( '/^avatar_/', '', $id ) ) ),
			);
		}

		uasort(
			$avatars,
			static function ( $a, $b ) {
				return strcasecmp( (string) $a['label'], (string) $b['label'] );
			}
		);

		return $avatars;
	}

	/**
	 * Saved avatar choice: default, bundled avatar id, or custom.
	 *
	 * @return string
	 */
	public static function get_avatar_choice() {
		$choice  = sanitize_key( (string) get_option( self::OPTION_PREFIX . 'avatar', '' ) );
		$stored  = trim( (string) get_option( self::OPTION_PREFIX . 'avatar_url', '' ) );

		if ( '' === $choice ) {
			return '' !== $stored ? 'custom' : 'default';
		}

		if ( 'custom' === $choice || 'default' === $choice ) {
			return $choice;
		}

		if ( in_array( $choice, self::get_bundled_avatar_ids(), true ) ) {
			return $choice;
		}

		return 'default';
	}

	/**
	 * Avatar choice for the settings UI.
	 *
	 * @return string
	 */
	public static function get_avatar_choice_for_settings() {
		return self::get_avatar_choice();
	}

	/**
	 * Custom avatar URL saved in settings (only when custom is selected).
	 *
	 * @return string
	 */
	public static function get_custom_avatar_url() {
		if ( 'custom' !== self::get_avatar_choice() ) {
			return '';
		}

		$url = trim( (string) get_option( self::OPTION_PREFIX . 'avatar_url', '' ) );

		return '' !== $url ? esc_url( $url ) : '';
	}

	/**
	 * Stored avatar URL for the current choice.
	 *
	 * @return string
	 */
	public static function get_stored_avatar_url() {
		$url = trim( (string) get_option( self::OPTION_PREFIX . 'avatar_url', '' ) );

		return '' !== $url ? esc_url( $url ) : '';
	}

	public static function is_valid_avatar_choice( $choice ) {
		$choice = sanitize_key( (string) $choice );

		if ( in_array( $choice, array( 'default', 'custom' ), true ) ) {
			return true;
		}

		return in_array( $choice, self::get_bundled_avatar_ids(), true );
	}

	/**
	 * Inline CSS for the avatar ring on the floating button.
	 *
	 * @param string $color Optional ring color.
	 * @return string
	 */
	public static function get_avatar_ring_style( $color = '' ) {
		$color = $color ? self::sanitize_avatar_bg_color( $color ) : self::get_avatar_bg_color();

		return sprintf(
			'box-shadow:0 0 0 4px %1$s,0 8px 24px rgba(0,0,0,0.18);',
			$color
		);
	}

	/**
	 * Avatar URL with a cache-busting query arg based on the saved choice.
	 *
	 * @return string
	 */
	public static function get_avatar_display_url() {
		$url    = self::get_avatar_url();
		$choice = self::get_avatar_choice();

		if ( '' === $url ) {
			return $url;
		}

		return add_query_arg(
			'maca_tjatt_avatar',
			rawurlencode( $choice ),
			$url
		);
	}

	/**
	 * Resolve the avatar URL for a saved choice.
	 *
	 * @param string $choice Avatar choice.
	 * @param string $stored Stored avatar URL.
	 * @return string
	 */
	public static function resolve_avatar_url( $choice, $stored = '' ) {
		$choice = sanitize_key( (string) $choice );
		$stored = trim( (string) $stored );

		if ( 'default' === $choice || '' === $choice ) {
			return esc_url( self::get_default_avatar_url() );
		}

		if ( 'custom' === $choice ) {
			$url = '' !== $stored ? esc_url( $stored ) : '';

			return '' !== $url ? $url : esc_url( self::get_default_avatar_url() );
		}

		if ( in_array( $choice, self::get_bundled_avatar_ids(), true ) ) {
			$bundled_url = self::get_bundled_avatar_url( $choice );

			if ( '' !== $bundled_url ) {
				return esc_url( $bundled_url );
			}
		}

		if ( '' !== $stored ) {
			return esc_url( $stored );
		}

		return esc_url( self::get_default_avatar_url() );
	}

	/**
	 * @return string
	 */
	public static function get_avatar_url() {
		return self::resolve_avatar_url( self::get_avatar_choice(), self::get_stored_avatar_url() );
	}

	/**
	 * Default background color behind the floating avatar button.
	 *
	 * @return string
	 */
	public static function get_default_avatar_bg_color() {
		return '#1f6f4a';
	}

	/**
	 * @return string
	 */
	public static function get_avatar_bg_color() {
		return self::sanitize_avatar_bg_color(
			(string) get_option( self::OPTION_PREFIX . 'avatar_bg_color', '' )
		);
	}

	/**
	 * @param string $color Raw color value.
	 * @return string
	 */
	public static function sanitize_avatar_bg_color( $color ) {
		$color     = trim( (string) $color );
		$sanitized = sanitize_hex_color( $color );

		if ( $sanitized ) {
			return $sanitized;
		}

		if ( preg_match( '/^#?[0-9a-fA-F]{6}$/', $color ) ) {
			$sanitized = sanitize_hex_color( '#' . ltrim( $color, '#' ) );

			if ( $sanitized ) {
				return $sanitized;
			}
		}

		if ( preg_match( '/^#?[0-9a-fA-F]{3}$/', $color ) ) {
			$short = ltrim( $color, '#' );
			$sanitized = sanitize_hex_color(
				sprintf(
					'#%1$s%1$s%2$s%2$s%3$s%3$s',
					$short[0],
					$short[1],
					$short[2]
				)
			);

			if ( $sanitized ) {
				return $sanitized;
			}
		}

		return self::get_default_avatar_bg_color();
	}

	/**
	 * @return array<string, string>
	 */
	public static function get_avatar_size_options() {
		return array(
			'small'  => __( 'Small', 'maca-tjatt' ),
			'medium' => __( 'Medium', 'maca-tjatt' ),
			'large'  => __( 'Large', 'maca-tjatt' ),
		);
	}

	/**
	 * @return string
	 */
	public static function get_avatar_size() {
		$size    = (string) get_option( self::OPTION_PREFIX . 'avatar_size', 'medium' );
		$allowed = array_keys( self::get_avatar_size_options() );

		return in_array( $size, $allowed, true ) ? $size : 'medium';
	}

	/**
	 * CSS modifier class for the selected avatar size.
	 *
	 * @return string
	 */
	public static function get_avatar_size_class_for_size( $size = '' ) {
		$map = array(
			'small'  => 'maca-tjatt--avatar-sm',
			'medium' => 'maca-tjatt--avatar-md',
			'large'  => 'maca-tjatt--avatar-lg',
		);

		if ( '' === $size ) {
			$size = self::get_avatar_size();
		}

		return isset( $map[ $size ] ) ? $map[ $size ] : $map['medium'];
	}

	/**
	 * CSS modifier class for the selected avatar size.
	 *
	 * @return string
	 */
	public static function get_avatar_size_class() {
		return self::get_avatar_size_class_for_size( self::get_avatar_size() );
	}

	/**
	 * Pixel dimensions for header and floating toggle avatars.
	 *
	 * @param string $size Optional size key.
	 * @return array{header: int, toggle: int}
	 */
	public static function get_avatar_dimensions_for_size( $size = '' ) {
		$map = array(
			'small'  => array(
				'header' => 32,
				'toggle' => 40,
			),
			'medium' => array(
				'header' => 44,
				'toggle' => 48,
			),
			'large'  => array(
				'header' => 56,
				'toggle' => 64,
			),
		);

		if ( '' === $size ) {
			$size = self::get_avatar_size();
		}

		return isset( $map[ $size ] ) ? $map[ $size ] : $map['medium'];
	}

	/**
	 * Pixel dimensions for header and floating toggle avatars.
	 *
	 * @return array{header: int, toggle: int}
	 */
	public static function get_avatar_dimensions() {
		return self::get_avatar_dimensions_for_size( self::get_avatar_size() );
	}

	/**
	 * @return string
	 */
	public static function get_chat_model() {
		$model = get_option( self::OPTION_PREFIX . 'chat_model', '' );

		return is_string( $model ) ? trim( $model ) : '';
	}

	/**
	 * @return int
	 */
	public static function get_rate_limit() {
		$limit = (int) get_option( self::OPTION_PREFIX . 'rate_limit', 30 );

		return max( 5, min( 200, $limit ) );
	}

	/**
	 * @return array<int, int>
	 */
	public static function get_page_ids() {
		$ids = get_option( self::OPTION_PREFIX . 'page_ids', array() );

		if ( ! is_array( $ids ) ) {
			return array();
		}

		$clean = array();

		foreach ( $ids as $id ) {
			$id = absint( $id );

			if ( $id > 0 ) {
				$clean[] = $id;
			}
		}

		return array_values( array_unique( $clean ) );
	}

	/**
	 * @return bool
	 */
	public static function index_all_pages() {
		return '1' === (string) get_option( self::OPTION_PREFIX . 'index_all_pages', '1' );
	}

	/**
	 * @return bool
	 */
	public static function index_posts() {
		return '1' === (string) get_option( self::OPTION_PREFIX . 'index_posts', '1' );
	}

	/**
	 * @return int
	 */
	public static function get_search_chunk_limit() {
		return 12;
	}

	/**
	 * @return float
	 */
	public static function get_search_score_threshold() {
		return 0.25;
	}

	/**
	 * @param array<string, array<string, int|string>> $schedules Cron schedules.
	 * @return array<string, array<string, int|string>>
	 */
	public static function add_cron_schedules( $schedules ) {
		$schedules['maca_tjatt_hourly'] = array(
			'interval' => HOUR_IN_SECONDS,
			'display'  => __( 'Every hour', 'maca-tjatt' ),
		);

		return $schedules;
	}

	/**
	 * @return string
	 */
	public static function get_custom_knowledge() {
		return trim( (string) get_option( self::OPTION_PREFIX . 'custom_knowledge', '' ) );
	}

	/**
	 * @return array<string, mixed>
	 */
	public static function get_index_status() {
		$status = get_option( self::OPTION_PREFIX . 'index_status', array() );

		return is_array( $status ) ? $status : array();
	}

	/**
	 * @param array<string, mixed> $status Status payload.
	 */
	public static function set_index_status( $status ) {
		update_option( self::OPTION_PREFIX . 'index_status', $status, false );
	}

	/**
	 * @param string $question Search query.
	 * @param string $title    Chunk title.
	 * @param string $content  Chunk content.
	 * @return float
	 */
	public static function keyword_relevance_score( $question, $title, $content ) {
		$terms = self::extract_search_terms( $question );

		if ( empty( $terms ) ) {
			return 0.0;
		}

		$title_lc   = self::normalize_search_text( $title );
		$content_lc = self::normalize_search_text( $content );
		$matches    = 0.0;

		foreach ( $terms as $term ) {
			if ( '' !== $title_lc && false !== strpos( $title_lc, $term ) ) {
				$matches += 1.5;
				continue;
			}

			if ( '' !== $content_lc && false !== strpos( $content_lc, $term ) ) {
				++$matches;
			}
		}

		return $matches / count( $terms );
	}

	/**
	 * @param string $text Source text.
	 * @return array<int, string>
	 */
	private static function extract_search_terms( $text ) {
		$text = self::normalize_search_text( $text );

		if ( '' === $text ) {
			return array();
		}

		$parts = preg_split( '/\s+/u', $text );
		$terms = array();

		if ( ! is_array( $parts ) ) {
			return array();
		}

		foreach ( $parts as $part ) {
			$part = trim( (string) $part );

			if ( strlen( $part ) < 3 ) {
				continue;
			}

			$terms[] = $part;
		}

		return array_values( array_unique( $terms ) );
	}

	/**
	 * @param string $text Source text.
	 * @return string
	 */
	private static function normalize_search_text( $text ) {
		$text = strtolower( trim( (string) $text ) );
		$text = preg_replace( '/[^\p{L}\p{N}\s]+/u', ' ', $text );

		return is_string( $text ) ? trim( preg_replace( '/\s+/u', ' ', $text ) ) : '';
	}

	/**
	 * @param string $question Guest question.
	 * @param int    $limit    Max results.
	 * @return array<int, object>
	 */
	public static function search_chunks( $question, $limit = 0 ) {
		if ( $limit <= 0 ) {
			$limit = self::get_search_chunk_limit();
		}

		$question = trim( (string) $question );

		if ( '' === $question ) {
			return array();
		}

		$chunks = Maca_Tjatt_DB::get_all_chunks();
		$scored = array();

		foreach ( $chunks as $chunk ) {
			$title   = isset( $chunk->title ) ? (string) $chunk->title : '';
			$content = isset( $chunk->content ) ? (string) $chunk->content : '';
			$score   = self::keyword_relevance_score( $question, $title, $content );

			if ( $score <= 0 ) {
				continue;
			}

			$scored[] = array(
				'score' => $score,
				'chunk' => $chunk,
			);
		}

		usort(
			$scored,
			static function ( $a, $b ) {
				if ( $a['score'] === $b['score'] ) {
					return 0;
				}

				return ( $a['score'] < $b['score'] ) ? 1 : -1;
			}
		);

		$results = array();

		foreach ( array_slice( $scored, 0, max( 1, (int) $limit ) ) as $item ) {
			if ( $item['score'] < self::get_search_score_threshold() ) {
				continue;
			}

			$results[] = $item['chunk'];
		}

		return $results;
	}

	/**
	 * @return string
	 */
	public static function build_system_prompt() {
		$site_name = get_bloginfo( 'name' );
		$assistant = self::get_display_name();

		$lines = array(
			sprintf(
				/* translators: 1: assistant name, 2: site name */
				__( 'You are %1$s, the digital assistant for %2$s.', 'maca-tjatt' ),
				$assistant,
				$site_name
			),
			__( 'Answer only using the CONTEXT provided in the user message.', 'maca-tjatt' ),
			__( 'If the answer is not in the context, say you do not know and suggest contacting the website owner.', 'maca-tjatt' ),
			__( 'Never invent facts, prices, dates, or policies.', 'maca-tjatt' ),
			__( 'Reply in the same language as the guest question (Swedish or English).', 'maca-tjatt' ),
			__( 'Keep answers concise (2–5 sentences).', 'maca-tjatt' ),
		);

		return implode( "\n", $lines );
	}

	/**
	 * @param array<int, object> $chunks Chunk rows.
	 * @return string
	 */
	public static function format_context( $chunks ) {
		$parts = array();

		foreach ( $chunks as $index => $chunk ) {
			$title   = isset( $chunk->title ) ? trim( (string) $chunk->title ) : '';
			$content = isset( $chunk->content ) ? trim( (string) $chunk->content ) : '';
			$type    = isset( $chunk->source_type ) ? (string) $chunk->source_type : '';

			if ( '' === $content ) {
				continue;
			}

			$header = '[' . ( $index + 1 ) . ']';

			if ( '' !== $type ) {
				$header .= ' (' . $type . ')';
			}

			if ( '' !== $title ) {
				$header .= ' ' . $title;
			}

			$parts[] = $header . "\n" . $content;
		}

		return implode( "\n\n", $parts );
	}

	/**
	 * @param array<int, array<string, string>> $sources Source rows.
	 * @return array<int, array{url: string, label: string}>
	 */
	public static function format_source_links( $sources ) {
		$links = array();
		$seen  = array();

		foreach ( $sources as $source ) {
			if ( ! is_array( $source ) ) {
				continue;
			}

			$url = isset( $source['url'] ) ? trim( (string) $source['url'] ) : '';

			if ( '' === $url || isset( $seen[ $url ] ) ) {
				continue;
			}

			if ( untrailingslashit( $url ) === untrailingslashit( home_url( '/' ) ) ) {
				continue;
			}

			$seen[ $url ] = true;
			$title        = isset( $source['title'] ) ? trim( (string) $source['title'] ) : '';

			$links[] = array(
				'url'   => $url,
				'label' => '' !== $title ? $title : __( 'Read more', 'maca-tjatt' ),
			);

			if ( count( $links ) >= 3 ) {
				break;
			}
		}

		return $links;
	}

	/**
	 * @param string                            $question Guest question.
	 * @param array<int, array<string, string>> $history  Prior turns.
	 * @param array<string, mixed>              $args     Optional flags.
	 * @return array{answer: string, sources: array<int, array<string, string>>, links: array<int, array{url: string, label: string}>}|WP_Error
	 */
	public static function ask( $question, $history = array(), $args = array() ) {
		$question = trim( (string) $question );

		if ( '' === $question ) {
			return new WP_Error( 'maca_tjatt_empty', __( 'Please enter a question.', 'maca-tjatt' ) );
		}

		if ( ! self::is_enabled() ) {
			return new WP_Error( 'maca_tjatt_disabled', __( 'The chatbot is not enabled.', 'maca-tjatt' ) );
		}

		if ( ! self::is_ai_available() ) {
			return new WP_Error( 'maca_tjatt_ai_unavailable', Maca_Tjatt_AI_Client::get_unavailable_message() );
		}

		if ( Maca_Tjatt_DB::count_chunks() <= 0 ) {
			return new WP_Error( 'maca_tjatt_no_index', __( 'The chat index has not been built yet.', 'maca-tjatt' ) );
		}

		$skip_rate_limit = ! empty( $args['skip_rate_limit'] );

		if ( ! $skip_rate_limit && ! self::rate_limit_ok() ) {
			return new WP_Error( 'maca_tjatt_rate_limit', __( 'Too many questions — please try again later.', 'maca-tjatt' ) );
		}

		$chunks  = self::search_chunks( $question );
		$context = self::format_context( $chunks );

		$user_message  = "CONTEXT:\n" . ( '' !== $context ? $context : __( 'No matching information found in the index.', 'maca-tjatt' ) );
		$user_message .= "\n\n" . __( 'QUESTION:', 'maca-tjatt' ) . ' ' . $question;

		$answer = Maca_Tjatt_AI_Client::generate_chat(
			$user_message,
			self::build_system_prompt(),
			is_array( $history ) ? $history : array(),
			self::get_chat_model()
		);

		if ( is_wp_error( $answer ) ) {
			return $answer;
		}

		if ( ! $skip_rate_limit ) {
			self::record_rate_limit_hit();
		}

		$sources = array();

		foreach ( $chunks as $chunk ) {
			$meta = array();

			if ( ! empty( $chunk->meta_json ) ) {
				$decoded = json_decode( (string) $chunk->meta_json, true );
				$meta    = is_array( $decoded ) ? $decoded : array();
			}

			$sources[] = array(
				'type'  => isset( $chunk->source_type ) ? (string) $chunk->source_type : '',
				'title' => isset( $chunk->title ) ? (string) $chunk->title : '',
				'url'   => isset( $meta['url'] ) ? (string) $meta['url'] : '',
			);
		}

		return array(
			'answer'  => $answer,
			'sources' => $sources,
			'links'   => self::format_source_links( $sources ),
		);
	}

	/**
	 * @return string
	 */
	private static function rate_limit_key() {
		$ip = 'unknown';

		if ( isset( $_SERVER['REMOTE_ADDR'] ) ) {
			$ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
		}

		return 'maca_tjatt_rl_' . md5( $ip );
	}

	/**
	 * @return bool
	 */
	public static function rate_limit_ok() {
		$count = (int) get_transient( self::rate_limit_key() );

		return $count < self::get_rate_limit();
	}

	/**
	 * Record one chat request.
	 */
	public static function record_rate_limit_hit() {
		$key   = self::rate_limit_key();
		$count = (int) get_transient( $key );
		set_transient( $key, $count + 1, HOUR_IN_SECONDS );
	}

	/**
	 * Schedule debounced reindex.
	 */
	public static function schedule_reindex() {
		if ( ! self::is_ai_available() ) {
			return;
		}

		if ( wp_next_scheduled( 'maca_tjatt_reindex_event' ) ) {
			return;
		}

		wp_schedule_single_event( time() + 30, 'maca_tjatt_reindex_event' );
	}

	/**
	 * Register daily reindex cron when enabled.
	 */
	public static function maybe_schedule_cron() {
		$hook     = 'maca_tjatt_daily_reindex';
		$schedule = 'maca_tjatt_hourly';

		if ( self::is_enabled() && self::is_ai_available() ) {
			$event = function_exists( 'wp_get_scheduled_event' ) ? wp_get_scheduled_event( $hook ) : null;

			if ( ! $event || $event->schedule !== $schedule ) {
				wp_clear_scheduled_hook( $hook );
				wp_schedule_event( time() + 300, $schedule, $hook );
			}

			return;
		}

		$timestamp = wp_next_scheduled( $hook );

		if ( $timestamp ) {
			wp_unschedule_event( $timestamp, $hook );
		}
	}
}
