<?php
/**
 * Main module class for Context Awareness.
 *
 * @package Replyan_AI
 */

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

require_once __DIR__ . '/class-replyan-ai-context-awareness-db.php';
require_once __DIR__ . '/class-replyan-ai-source-provider-interface.php';
require_once __DIR__ . '/class-replyan-ai-post-source-provider.php';
require_once __DIR__ . '/class-replyan-ai-page-source-provider.php';
require_once __DIR__ . '/class-replyan-ai-product-source-provider.php';
require_once __DIR__ . '/class-replyan-ai-cpt-source-provider.php';
require_once __DIR__ . '/class-replyan-ai-source-manager.php';
require_once __DIR__ . '/class-replyan-ai-content-cleaner.php';
require_once __DIR__ . '/class-replyan-ai-content-chunker.php';
require_once __DIR__ . '/class-replyan-ai-embedding-service.php';
require_once __DIR__ . '/class-replyan-ai-vector-storage.php';
require_once __DIR__ . '/class-replyan-ai-sync-service.php';

final class Replyan_AI_Context_Awareness {

	public function register_hooks(): void {
		add_filter( 'replyan_ai_system_prompt', array( $this, 'inject_retrieved_context' ), 20, 4 );
		add_action( 'rest_api_init', array( $this, 'register_rest_routes' ) );

		// Auto-delete chunks when post is deleted or unpublished
		add_action( 'before_delete_post', array( $this, 'handle_post_deletion' ) );
		add_action( 'transition_post_status', array( $this, 'handle_post_transition' ), 10, 3 );
	}

	/**
	 * Automatically retrieve and inject relevant knowledge base chunks into the system prompt.
	 *
	 * @param string $prompt
	 * @param string $session_id
	 * @param int    $agent_id
	 * @param string $message
	 * @return string
	 */
	public function inject_retrieved_context( string $prompt, string $session_id, int $agent_id, string $message = '' ): string {
		if ( empty( trim( $message ) ) ) {
			return $prompt;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Missing
		if ( ! empty( $_POST['replyan_ai_skip_rag'] ) ) {
			return $prompt;
		}

		global $wpdb;
		$table = $wpdb->prefix . 'replyan_ai_knowledge_chunks';

		// If knowledge chunks table is empty (e.g. on fresh install before sync), skip context retrieval immediately
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$has_chunks = $wpdb->get_var( "SELECT 1 FROM {$table} LIMIT 1" );
		if ( ! $has_chunks ) {
			return $prompt;
		}

		$settings = Replyan_AI_Settings::get();

		// 1. Conversational Query Expansion for short follow-up messages
		$search_query = $message;
		if ( ! empty( $session_id ) && str_word_count( $message ) <= 7 ) {
			$recent_messages = Replyan_AI_DB::get_messages( $session_id );
			if ( ! empty( $recent_messages ) ) {
				$user_messages = array();
				foreach ( $recent_messages as $m ) {
					if ( isset( $m['role'] ) && 'user' === $m['role'] && ! empty( $m['content'] ) ) {
						$user_messages[] = $m['content'];
					}
				}
				$msg_count     = count( $user_messages );
				$prev_user_msg = '';
				if ( $msg_count >= 2 && $user_messages[ $msg_count - 1 ] === $message ) {
					$prev_user_msg = $user_messages[ $msg_count - 2 ];
				} elseif ( $msg_count >= 1 && $user_messages[ $msg_count - 1 ] !== $message ) {
					$prev_user_msg = $user_messages[ $msg_count - 1 ];
				}
				if ( ! empty( $prev_user_msg ) ) {
					$search_query = $prev_user_msg . ' ' . $message;
				}
			}
		}

		// 2. Vector Search
		$vector_chunks = array();
		try {
			$embedding_service = new Replyan_AI_Embedding_Service( $settings );
			$query_embedding   = $embedding_service->generate( $search_query );

			if ( ! is_wp_error( $query_embedding ) && ! empty( $query_embedding ) && is_array( $query_embedding ) ) {
				$vector_storage = new Replyan_AI_Vector_Storage();
				$vector_chunks  = $vector_storage->search_similar_chunks( $query_embedding, 5 );
			}
		} catch ( Throwable $e ) {
			// Fail gracefully if embedding service encounters API or provider error
		}

		// 3. Keyword / Numeric Exact Match Search (Hybrid Search with Weighted Ranking)
		$keyword_chunks = array();

		// Extract numeric tokens (Zip codes, IDs, prices - e.g. 19107, 90210)
		$numeric_tokens = array();
		if ( preg_match_all( '/\b\d{3,}\b/', $message, $num_matches ) ) {
			$numeric_tokens = array_unique( $num_matches[0] );
		}

		// Extract alphabetic words
		$valid_words = array();
		if ( preg_match_all( '/\b[a-zA-Z]{3,}\b/', $message, $word_matches ) ) {
			$raw_words  = array_unique( $word_matches[0] );
			$stop_words = array(
				// Common conversational stopwords
				'about', 'there', 'where', 'which', 'what', 'when', 'how', 'any', 'the', 'and', 'for',
				'are', 'can', 'you', 'with', 'this', 'that', 'have', 'does', 'will', 'from', 'your',
				'please', 'find', 'show', 'want', 'need', 'tell', 'give', 'know', 'some', 'more', 'like',
				'into', 'been', 'were', 'they', 'them', 'their', 'then', 'than', 'also', 'just', 'such',
				'much', 'many', 'very', 'could', 'should', 'would', 'hello', 'available', 'options',
				// Knowledge base structural labels (prevent matching all chunks)
				'zip', 'code', 'postal', 'street', 'address', 'link', 'http', 'https', 'localhost',
				'title', 'description', 'place', 'places', 'post', 'posts', 'page', 'pages', 'property',
				'properties', 'source', 'state', 'country', 'region'
			);

			foreach ( $raw_words as $w ) {
				$w_lower = strtolower( $w );
				if ( ! in_array( $w_lower, $stop_words, true ) ) {
					$valid_words[] = $w;
				}
			}
		}

		if ( ! empty( $numeric_tokens ) || ! empty( $valid_words ) ) {
			$score_parts  = array();
			$where_parts  = array();
			$score_params = array();
			$where_params = array();

			// High weight for exact numeric codes (zip codes, model numbers, IDs)
			foreach ( $numeric_tokens as $num ) {
				$like           = '%' . $wpdb->esc_like( $num ) . '%';
				$score_parts[]  = '(CASE WHEN content LIKE %s THEN 8 WHEN title LIKE %s THEN 8 ELSE 0 END)';
				$score_params[] = $like;
				$score_params[] = $like;
				$where_parts[]  = '(title LIKE %s OR content LIKE %s)';
				$where_params[] = $like;
				$where_params[] = $like;
			}

			// Specific word tokens (title match = 4 pts, content match = 1.5 pts)
			foreach ( $valid_words as $word ) {
				$like           = '%' . $wpdb->esc_like( $word ) . '%';
				$score_parts[]  = '(CASE WHEN title LIKE %s THEN 4 WHEN content LIKE %s THEN 1.5 ELSE 0 END)';
				$score_params[] = $like;
				$score_params[] = $like;
				$where_parts[]  = '(title LIKE %s OR content LIKE %s)';
				$where_params[] = $like;
				$where_params[] = $like;
			}

			if ( ! empty( $where_parts ) ) {
				$score_sql  = implode( ' + ', $score_parts );
				$where_sql  = implode( ' OR ', $where_parts );
				$all_params = array_merge( $score_params, $where_params );

				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, PluginCheck.Security.DirectDB.UnescapedDBParameter
				$kw_results = $wpdb->get_results( $wpdb->prepare( "SELECT id, post_id, title, content, url, ({$score_sql}) AS relevance_score FROM {$table} WHERE {$where_sql} ORDER BY relevance_score DESC LIMIT 5", ...$all_params ), ARRAY_A );

				if ( ! empty( $kw_results ) ) {
					foreach ( $kw_results as $row ) {
						if ( (float) ( $row['relevance_score'] ?? 0 ) > 0 ) {
							$row['similarity']           = 0.99;
							$keyword_chunks[ $row['id'] ] = $row;
						}
					}
				}
			}
		}

		// 4. Merge & Deduplicate chunks
		$merged_chunks = $keyword_chunks;
		foreach ( $vector_chunks as $chunk ) {
			$id  = $chunk['id'];
			$sim = (float) ( $chunk['similarity'] ?? 0.0 );
			if ( ! isset( $merged_chunks[ $id ] ) && $sim >= 0.22 ) {
				$merged_chunks[ $id ] = $chunk;
			}
		}

		// Fallback: If threshold was borderline and we have at least something >= 0.20
		if ( empty( $merged_chunks ) && ! empty( $vector_chunks ) ) {
			if ( isset( $vector_chunks[0]['similarity'] ) && (float) $vector_chunks[0]['similarity'] >= 0.20 ) {
				$merged_chunks = array_slice( $vector_chunks, 0, 2 );
			}
		}

		// Cap max context chunks to top 5
		if ( count( $merged_chunks ) > 5 ) {
			$merged_chunks = array_slice( $merged_chunks, 0, 5, true );
		}

		if ( empty( $merged_chunks ) ) {
			return $prompt;
		}

		$context_text  = "\n\n=== RELEVANT KNOWLEDGE BASE CONTEXT ===\n";
		$context_text .= "The following information was retrieved from the website's database. Always use this retrieved context as the factual source of truth when answering questions about listings, products, services, locations, or details:\n\n";

		foreach ( array_values( $merged_chunks ) as $index => $chunk ) {
			$post_id   = (int) ( $chunk['post_id'] ?? 0 );
			$permalink = $post_id ? get_permalink( $post_id ) : ( $chunk['url'] ?? '' );
			if ( empty( $permalink ) && ! empty( $chunk['url'] ) ) {
				$permalink = $chunk['url'];
			}
			$title     = $chunk['title'] ?? 'Document ' . ( $index + 1 );

			$source_line = 'Source: ' . $title;
			if ( $permalink ) {
				$source_line .= ' - ' . $permalink;
			}

			$context_text .= '--- [' . $source_line . "] ---\n";
			$context_text .= trim( $chunk['content'] ?? '' ) . "\n\n";
		}

		$context_text .= "CRITICAL INSTRUCTIONS FOR RETRIEVED CONTEXT:\n";
		$context_text .= "1. Answer the user's question directly, accurately, and politely using the retrieved information above.\n";
		$context_text .= "2. Do NOT output placeholder or waiting phrases like 'Let me check for you', 'Please hold on', or 'One moment'. Provide the complete, helpful answer immediately.\n";
		$context_text .= "3. Mention relevant details (title, key features, pricing, location) and ALWAYS include the link/URL as a clickable markdown link (e.g. [Title](URL)) when recommending or referring to a listing, product, post, or page.\n";
		$context_text .= "4. If the retrieved information does not fully answer the user's inquiry, answer as best as possible from the facts and politely offer further assistance.\n";

		return $prompt . $context_text;
	}

	public function handle_post_deletion( $post_id ): void {
		$vector_storage = new Replyan_AI_Vector_Storage();
		$vector_storage->delete_chunks( $post_id );
	}

	public function handle_post_transition( $new_status, $old_status, $post ): void {
		if ( 'publish' !== $new_status ) {
			$vector_storage = new Replyan_AI_Vector_Storage();
			$vector_storage->delete_chunks( $post->ID );
		}
	}

	public function register_rest_routes(): void {
		register_rest_route( 'replyan/v1', '/admin/context-awareness/stats', array(
			'methods'             => WP_REST_Server::READABLE,
			'callback'            => array( $this, 'get_stats' ),
			'permission_callback' => function () {
				return current_user_can( 'manage_options' );
			},
		) );

		register_rest_route( 'replyan/v1', '/admin/context-awareness/sync/init', array(
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => array( $this, 'sync_init' ),
			'permission_callback' => function () {
				return current_user_can( 'manage_options' );
			},
		) );

		register_rest_route( 'replyan/v1', '/admin/context-awareness/sync/process', array(
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => array( $this, 'sync_process' ),
			'permission_callback' => function () {
				return current_user_can( 'manage_options' );
			},
		) );
		register_rest_route( 'replyan/v1', '/admin/context-awareness/cpt-list', array(
			'methods'             => WP_REST_Server::READABLE,
			'callback'            => array( $this, 'get_cpt_list' ),
			'permission_callback' => function () {
				return current_user_can( 'manage_options' );
			},
		) );
		register_rest_route( 'replyan/v1', '/admin/context-awareness/cpt-fields', array(
			'methods'             => WP_REST_Server::READABLE,
			'callback'            => array( $this, 'get_cpt_fields' ),
			'permission_callback' => function () {
				return current_user_can( 'manage_options' );
			},
		) );
		register_rest_route( 'replyan/v1', '/admin/context-awareness/save-cpt-fields', array(
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => array( $this, 'save_cpt_fields' ),
			'permission_callback' => function () {
				return current_user_can( 'manage_options' );
			},
		) );
	}

	public function get_stats(): WP_REST_Response {
		$posts = wp_count_posts( 'post' );
		$pages = wp_count_posts( 'page' );

		$post_count = isset( $posts->publish ) ? (int) $posts->publish : 0;
		$page_count = isset( $pages->publish ) ? (int) $pages->publish : 0;
		$product_count = 0;

		if ( class_exists( 'WooCommerce' ) ) {
			$products = wp_count_posts( 'product' );
			$product_count = isset( $products->publish ) ? (int) $products->publish : 0;
		}

		// Calculate CPT counts (only posts with non-empty content)
		$cpts = array();
		$post_types = get_post_types( array( 'public' => true, '_builtin' => false ), 'objects' );
		foreach ( $post_types as $pt ) {
			if ( 'product' === $pt->name ) {
				continue; // Products handled separately
			}
			$cpts[ $pt->name ] = $this->count_posts_with_content( $pt->name );
		}

		return rest_ensure_response( array(
			'posts'    => $post_count,
			'pages'    => $page_count,
			'products' => $product_count,
			'cpts'     => $cpts,
			'total'    => $post_count + $page_count + $product_count + array_sum( $cpts ),
		) );
	}

	public function get_cpt_list(): WP_REST_Response {
		$post_types = get_post_types( array( 'public' => true, '_builtin' => false ), 'objects' );
		$list = array();
		
		foreach ( $post_types as $pt ) {
			if ( 'product' === $pt->name ) {
				continue; // Products handled separately
			}
			
			$list[] = array(
				'slug'  => $pt->name,
				'label' => $pt->label,
				'count' => $this->count_posts_with_content( $pt->name ),
			);
		}

		return rest_ensure_response( $list );
	}

	public function sync_init(): WP_REST_Response|WP_Error {
		try {
			$sync_service = new Replyan_AI_Sync_Service();
			$sync_service->validate();
			$documents = $sync_service->get_documents();

			return rest_ensure_response( array(
				'documents' => $documents,
				'total'     => count( $documents ),
			) );
		} catch ( Exception $e ) {
			return new WP_Error( 'sync_init_failed', $e->getMessage(), array( 'status' => 400 ) );
		}
	}

	public function sync_process( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		try {
			$document = $request->get_json_params();

			if ( empty( $document['source_id'] ) || empty( $document['content'] ) ) {
				throw new Exception( 'Invalid document data.' );
			}

			$sync_service = new Replyan_AI_Sync_Service();
			$result       = $sync_service->process_document( $document );

			if ( 'error' === $result['status'] ) {
				return new WP_Error( 'sync_process_failed', $result['message'], array( 'status' => 500 ) );
			}

			return rest_ensure_response( $result );
		} catch ( Exception $e ) {
			return new WP_Error( 'sync_process_failed', $e->getMessage(), array( 'status' => 400 ) );
		}
	}

	/**
	 * Discover taxonomies and custom fields for a specific CPT.
	 *
	 * @param WP_REST_Request $request The REST request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_cpt_fields( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		global $wpdb;

		$post_type = sanitize_key( $request->get_param( 'post_type' ) );
		if ( empty( $post_type ) || ! post_type_exists( $post_type ) ) {
			return new WP_Error( 'invalid_post_type', 'Custom Post Type does not exist.', array( 'status' => 404 ) );
		}

		$pt = get_post_type_object( $post_type );

		// 1. Discover Taxonomies
		$taxonomies = array();
		$raw_tax    = get_object_taxonomies( $post_type, 'objects' );
		$ignored    = array( 'nav_menu', 'link_category', 'post_format', 'wp_theme', 'wp_template_part_area' );
		foreach ( $raw_tax as $tax ) {
			if ( in_array( $tax->name, $ignored, true ) || ! $tax->public ) {
				continue;
			}
			$count = wp_count_terms( array( 'taxonomy' => $tax->name, 'hide_empty' => false ) );
			$taxonomies[] = array(
				'name'  => $tax->name,
				'label' => ! empty( $tax->label ) ? $tax->label : ucwords( str_replace( array( '-', '_' ), ' ', $tax->name ) ),
				'count' => is_numeric( $count ) ? (int) $count : 0,
			);
		}

		// 2. Discover Custom Fields
		$custom_fields   = array();
		$discovered_keys = array();

		// 2a. GeoDirectory definitions table
		$geodir_cf_table = $wpdb->prefix . 'geodir_custom_fields';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $geodir_cf_table ) ) === $geodir_cf_table ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
			$cf_rows = $wpdb->get_results( $wpdb->prepare( "SELECT htmlvar_name, admin_title, frontend_title, field_type FROM {$geodir_cf_table} WHERE post_type = %s AND is_active = 1 ORDER BY sort_order ASC", $post_type ) );
			$skip_cf = array( 'post_title', 'post_content', 'post_tags', 'post_category', 'post_images' );
			foreach ( $cf_rows as $row ) {
				if ( in_array( $row->htmlvar_name, $skip_cf, true ) ) {
					continue;
				}
				$lbl = ! empty( $row->frontend_title ) ? $row->frontend_title : ( ! empty( $row->admin_title ) ? $row->admin_title : $row->htmlvar_name );
				$custom_fields[] = array(
					'key'    => $row->htmlvar_name,
					'label'  => $lbl,
					'source' => 'GeoDirectory Field',
				);
				$discovered_keys[ $row->htmlvar_name ] = true;
			}
		}

		// 2b. Detail Table Columns (GeoDirectory / Custom directory tables)
		$detail_table = $wpdb->prefix . 'geodir_' . $post_type . '_detail';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter
		if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $detail_table ) ) === $detail_table ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
			$cols        = $wpdb->get_col( "DESCRIBE {$detail_table}" );
			$system_cols = array(
				'post_id', 'post_title', 'post_content', 'post_excerpt', 'post_status', 'comment_status',
				'ping_status', 'post_password', 'post_name', 'to_ping', 'pinged', 'post_modified', 'post_modified_gmt',
				'post_date', 'post_date_gmt', 'post_content_filtered', 'post_parent', 'guid', 'menu_order',
				'post_type', 'post_mime_type', 'comment_count', 'post_author', '_search_title', 'post_tags',
				'post_category', 'default_category', 'submit_ip', 'featured', 'featured_image', 'overall_rating',
				'rating_count', 'latitude', 'longitude', 'mapview', 'mapzoom',
			);
			$labels      = array(
				'street'  => 'Street Address',
				'street2' => 'Address Line 2',
				'city'    => 'City',
				'region'  => 'Region / State',
				'country' => 'Country',
				'zip'     => 'Zip / Postal Code',
				'phone'   => 'Phone',
				'email'   => 'Email',
				'website' => 'Website',
				'price'   => 'Price',
			);
			foreach ( $cols as $col ) {
				if ( in_array( $col, $system_cols, true ) || isset( $discovered_keys[ $col ] ) ) {
					continue;
				}
				$custom_fields[] = array(
					'key'    => $col,
					'label'  => isset( $labels[ $col ] ) ? $labels[ $col ] : ucwords( str_replace( array( '-', '_' ), ' ', $col ) ),
					'source' => 'Directory Detail',
				);
				$discovered_keys[ $col ] = true;
			}
		}

		// 2c. ACF Fields
		if ( function_exists( 'acf_get_field_groups' ) && function_exists( 'acf_get_fields' ) ) {
			$groups = acf_get_field_groups( array( 'post_type' => $post_type ) );
			foreach ( $groups as $group ) {
				$fields = acf_get_fields( $group['key'] );
				if ( is_array( $fields ) ) {
					foreach ( $fields as $f ) {
						if ( ! empty( $f['name'] ) && ! isset( $discovered_keys[ $f['name'] ] ) ) {
							$custom_fields[] = array(
								'key'    => $f['name'],
								'label'  => ! empty( $f['label'] ) ? $f['label'] : $f['name'],
								'source' => 'ACF Field',
							);
							$discovered_keys[ $f['name'] ] = true;
						}
					}
				}
			}
		}

		// 2d. WordPress Post Meta (wp_postmeta)
		$recent_posts = get_posts( array(
			'post_type'      => $post_type,
			'posts_per_page' => 20,
			'fields'         => 'ids',
			'post_status'    => 'publish',
		) );

		if ( ! empty( $recent_posts ) ) {
			$ids_in             = implode( ',', array_map( 'intval', $recent_posts ) );
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			$meta_keys          = $wpdb->get_col( "SELECT DISTINCT meta_key FROM {$wpdb->postmeta} WHERE post_id IN ({$ids_in})" );
			$excluded_exact     = array(
				'created_at', 'created_date', 'created_time', 'creation_date', 'updated_at',
				'updated_date', 'updated_time', 'modified_date', 'edit_date', 'post_date',
				'revision_id', 'import_id', 'sync_status', 'import_source', 'author_id',
				'user_id', 'views', 'views_count', 'post_views_count', 'rating_count',
				'menu_order', 'rule', 'layout', 'format',
			);
			$sensitive_patterns = array(
				'password', 'secret', 'token', 'api_key', 'auth', 'nonce', 'card', 'cvv',
				'ssn', 'private', 'internal', 'billing_', 'stripe', 'paypal', 'session',
				'elementor', 'divi_', 'vc_', 'yoast', 'aioseo', 'rank_math', 'schema',
			);

			foreach ( $meta_keys as $mk ) {
				if ( str_starts_with( $mk, '_' ) ) {
					continue;
				}
				$lower = strtolower( $mk );
				if ( in_array( $lower, $excluded_exact, true ) ) {
					continue;
				}
				$is_sens = false;
				foreach ( $sensitive_patterns as $p ) {
					if ( str_contains( $lower, $p ) ) {
						$is_sens = true;
						break;
					}
				}
				if ( $is_sens || isset( $discovered_keys[ $mk ] ) ) {
					continue;
				}

				$custom_fields[] = array(
					'key'    => $mk,
					'label'  => ucwords( str_replace( array( '-', '_' ), ' ', $mk ) ),
					'source' => 'Post Meta',
				);
				$discovered_keys[ $mk ] = true;
			}
		}

		$settings     = Replyan_AI_Settings::get();
		$saved_config = isset( $settings['cpt_fields_config'][ $post_type ] ) ? $settings['cpt_fields_config'][ $post_type ] : null;

		return rest_ensure_response( array(
			'post_type'     => $post_type,
			'label'         => $pt->label,
			'has_content'   => post_type_supports( $post_type, 'editor' ),
			'taxonomies'    => $taxonomies,
			'custom_fields' => $custom_fields,
			'saved_config'  => $saved_config,
		) );
	}

	/**
	 * Save CPT fields preference via REST API.
	 *
	 * @param WP_REST_Request $request REST request.
	 * @return WP_REST_Response
	 */
	public function save_cpt_fields( WP_REST_Request $request ): WP_REST_Response {
		$params    = $request->get_json_params();
		$post_type = sanitize_key( $params['post_type'] ?? '' );
		$config    = isset( $params['config'] ) && is_array( $params['config'] ) ? $params['config'] : null;

		if ( empty( $post_type ) || null === $config ) {
			return rest_ensure_response( array(
				'success' => false,
				'message' => __( 'Invalid parameters provided.', 'replyan-ai' ),
			) );
		}

		$settings = Replyan_AI_Settings::get();
		if ( ! isset( $settings['cpt_fields_config'] ) || ! is_array( $settings['cpt_fields_config'] ) ) {
			$settings['cpt_fields_config'] = array();
		}

		$sanitized_config = array(
			'include_content'   => ! empty( $config['include_content'] ),
			'include_permalink' => ! empty( $config['include_permalink'] ),
			'taxonomies'        => isset( $config['taxonomies'] ) && is_array( $config['taxonomies'] ) ? array_values( array_map( 'sanitize_text_field', $config['taxonomies'] ) ) : array(),
			'custom_fields'     => isset( $config['custom_fields'] ) && is_array( $config['custom_fields'] ) ? array_values( array_map( 'sanitize_text_field', $config['custom_fields'] ) ) : array(),
		);

		$settings['cpt_fields_config'][ $post_type ] = $sanitized_config;
		update_option( Replyan_AI_Settings::OPTION_NAME, $settings );

		return rest_ensure_response( array(
			'success'      => true,
			'post_type'    => $post_type,
			'saved_config' => $sanitized_config,
			'all_configs'  => $settings['cpt_fields_config'],
		) );
	}

	/**
	 * Count published posts for a given post type.
	 *
	 * @param string $post_type Post type slug.
	 * @return int
	 */
	private function count_posts_with_content( string $post_type ): int {
		$counts = wp_count_posts( $post_type );
		return isset( $counts->publish ) ? (int) $counts->publish : 0;
	}
}
