<?php //phpcs:ignore
/**
 * Algolia gutenberg blocks.
 * Description: Adds Algolia search functionality to the main plugin by White Label Coders.
 * Text Domain: wlc-search-integration-with-algolia.
 *
 * @package WLC\Algolia
 */

namespace wlc_algolia;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Class Algolia_Blocks
 *
 * A class for integrating Algolia search with WordPress blocks.
 */
final class Algolia_Blocks { //phpcs:ignore

	/**
	 * Initialize the Algolia Blocks plugin.
	 */
	public static function init() {
		// Register hooks and actions during initialization.
		add_action( 'init', array( __CLASS__, 'register_algolia_blocks' ) );
		// Add your custom REST API endpoint for Algolia API keys.
		add_action( 'rest_api_init', array( __CLASS__, 'register_algolia_api_endpoint' ) );
	}

	/**
	 * Register blocks.
	 */
	public static function register_algolia_blocks() {
		// Add Algolia block category.
		add_filter( 'block_categories_all', array( __CLASS__, 'add_algolia_block_category' ) );

		// Register only active top-level Algolia blocks.
		register_block_type( plugin_dir_path( __DIR__ ) . '../blocks/build/blocks/search-page' );
		register_block_type( plugin_dir_path( __DIR__ ) . '../blocks/build/blocks/autocomplete' );
		register_block_type( plugin_dir_path( __DIR__ ) . '../blocks/build/blocks/agent-chat' );
		// Register inner blocks used by search-page templates.
		register_block_type( plugin_dir_path( __DIR__ ) . '../blocks/build/blocks/result-title' );
		register_block_type( plugin_dir_path( __DIR__ ) . '../blocks/build/blocks/result-image' );
		register_block_type( plugin_dir_path( __DIR__ ) . '../blocks/build/blocks/result-excerpt' );
		register_block_type( plugin_dir_path( __DIR__ ) . '../blocks/build/blocks/result-meta' );
		register_block_type( plugin_dir_path( __DIR__ ) . '../blocks/build/blocks/result-field' );
		register_block_type( plugin_dir_path( __DIR__ ) . '../blocks/build/blocks/result-button' );
	}

	/**
	 * Add Algolia block category.
	 *
	 * @param array $categories Existing block categories.
	 * @return array Modified block categories.
	 */
	public static function add_algolia_block_category( $categories ) {
		array_unshift(
			$categories,
			array(
				'slug'  => 'wlc-algolia-blocks',
				'title' => 'WLC Search blocks',
			)
		);
		return $categories;
	}

	/**
	 * Register custom REST API endpoint for Algolia API keys.
	 */
	public static function register_algolia_api_endpoint() {
		// Public endpoint by design: exposes only the Application ID, the
		// search-only API key and the index name, which the frontend search
		// UI needs for anonymous visitors. The write/admin key is never returned.
		register_rest_route(
			'wlc-algolia/v1',
			'/api-keys/',
			array(
				'methods'             => 'GET',
				'callback'            => array( __CLASS__, 'get_algolia_api_keys' ),
				'permission_callback' => '__return_true',
			)
		);

		register_rest_route(
			'wlc-algolia/v1',
			'/autocomplete-settings/',
			array(
				'methods'             => 'GET',
				'callback'            => array( __CLASS__, 'get_autocomplete_settings' ),
				'permission_callback' => '__return_true',
			)
		);

		register_rest_route(
			'wlc-algolia/v1',
			'/facets/',
			array(
				'methods'             => 'GET',
				'callback'            => array( __CLASS__, 'get_facets' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'post_type' => array(
						'required'          => false,
						'default'           => 'post',
						'validate_callback' => function ( $param ) {
							return is_string( $param );
						},
					),
				),
			)
		);

		register_rest_route(
			'wlc-algolia/v1',
			'/index-keys/',
			array(
				'methods'             => 'GET',
				'callback'            => array( __CLASS__, 'get_index_keys' ),
				// Sample values from indexed records may include non-public data.
				// Restrict to administrators who can manage plugin settings.
				'permission_callback' => function () {
					return current_user_can( 'manage_options' );
				},
				'args'                => array(
					'post_type' => array(
						'required'          => false,
						'default'           => 'post',
						'validate_callback' => function ( $param ) {
							return is_string( $param );
						},
					),
				),
			)
		);

		register_rest_route(
			'wlc-algolia/v1',
			'/indices/',
			array(
				'methods'             => 'GET',
				'callback'            => array( __CLASS__, 'get_algolia_indices' ),
				// Uses the Write API key server-side; admin-only.
				'permission_callback' => function () {
					return current_user_can( 'manage_options' );
				},
			)
		);

		register_rest_route(
			'wlc-algolia/v1',
			'/permalink/',
			array(
				'methods'             => 'GET',
				'callback'            => array( __CLASS__, 'get_permalink_by_post_id' ),
				'permission_callback' => '__return_true',
				'args'                => array(
					'post_id' => array(
						'required'          => true,
						'validate_callback' => static function ( $param ) {
							return is_numeric( $param ) && absint( $param ) > 0;
						},
					),
				),
			)
		);
	}

	/**
	 * Get Autocomplete Settings.
	 *
	 * @return \WP_Error|\WP_HTTP_Response|\WP_REST_Response
	 */
	public static function get_autocomplete_settings() {
		$autocomplete_configuration = (array) get_option( 'wlc_algolia_settings_autocomplete' ); //phpcs:ignore
		$config                     = array();

		foreach ( $autocomplete_configuration as $post_type => $autocomplete_config ) {
			if ( '1' === $autocomplete_config ) {
				continue;
			}
			$config[] = array(
				'post_type'       => ucfirst( $post_type ),
				'max_suggestions' => esc_attr( $autocomplete_config['max-suggestions'] ),
				'index_name'      => esc_attr( $autocomplete_config['index_name'] ),
			);
		}

		if ( isset( $autocomplete_configuration['enable-autocomplete'] ) ) {
			$config['enable_autocomplete'] = '1';
		}

		$autocomplete_settings = array( 'autocomplete_settings' => $config );

		return rest_ensure_response( $autocomplete_settings );
	}

	/**
	 * Callback function to retrieve Algolia API keys.
	 *
	 * @return WP_REST_Response Response containing API keys.
	 */
	public static function get_algolia_api_keys() {
		// Include the file that contains the Algolia_Settings_Options class.
		require_once plugin_dir_path( __DIR__ ) . 'classes/class-algolia-settings-options.php';

		// Retrieve Algolia API keys using your Algolia_Settings_Options class.
		$algolia_settings = new Algolia_Settings_Options();
		$api_keys         = array(
			'applicationId' => $algolia_settings->get_application_id(),
			'searchApiKey'  => $algolia_settings->get_search_api_key(),
			'indexName'     => $algolia_settings->get_index_name(),
		);

		// Prepare and send REST response.
		$response_data = array( 'api_keys' => $api_keys );

		return rest_ensure_response( $response_data );
	}

	/**
	 * Get list of all Algolia indices (for Query Suggestions index picker, etc.).
	 * Uses Write API key; requires manage_options capability.
	 *
	 * @return \WP_REST_Response|\WP_Error
	 */
	public static function get_algolia_indices() {
		require_once plugin_dir_path( __DIR__ ) . 'classes/class-algolia-settings-options.php';
		$settings  = new Algolia_Settings_Options();
		$app_id    = $settings->get_application_id();
		$write_key = $settings->get_write_api_key();

		$indices = array();
		if ( empty( $app_id ) || empty( $write_key ) ) {
			return rest_ensure_response( array( 'indices' => array() ) );
		}

		try {
			$client = \Algolia\AlgoliaSearch\Api\SearchClient::create( $app_id, $write_key );
			if ( method_exists( $client, 'listIndices' ) ) {
				// The v4 client returns responses as associative arrays.
				$list  = $client->listIndices();
				$items = isset( $list['items'] ) && is_array( $list['items'] ) ? $list['items'] : array();
				foreach ( $items as $item ) {
					$name = isset( $item['name'] ) ? (string) $item['name'] : '';
					if ( '' !== $name ) {
						$indices[] = array( 'name' => $name );
					}
				}
			}
		} catch ( \Throwable $e ) {
			// Never expose the raw exception message to the client: it may leak
			// credentials or infrastructure details. Log it for admins instead.
			error_log( 'WLC Algolia: listing indices failed: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			return new \WP_Error(
				'wlc_algolia_indices_error',
				__( 'Could not fetch the list of indices from Algolia.', 'wlc-search-integration-with-algolia' ),
				array( 'status' => 500 )
			);
		}

		return rest_ensure_response( array( 'indices' => $indices ) );
	}

	/**
	 * Get facets for a post type.
	 *
	 * @param \WP_REST_Request $request Request object.
	 * @return \WP_Error|\WP_HTTP_Response|\WP_REST_Response
	 */
	public static function get_facets( $request ) {
		$post_type_param = $request->get_param( 'post_type' );
		$post_type       = sanitize_key( $post_type_param ? $post_type_param : 'post' );
		$facets          = array();

		$cpt_settings = get_option( 'wlc_algolia_settings_cpt_' . $post_type, array() );
		if ( is_array( $cpt_settings ) && ! empty( $cpt_settings ) ) {
			require_once plugin_dir_path( __DIR__ ) . 'classes/class-algolia-index-helper.php';
			$selected_fields = Algolia_Index_Helper::wlc_prepare_selected_fields( $cpt_settings, false );

			foreach ( $selected_fields as $field => $options ) {
				if ( 'taxonomies' === $field ) {
					continue;
				}
				if ( ! empty( $options['index'] ) && '1' === $options['index'] ) {
					if ( isset( $options['facet'] ) && '1' === $options['facet'] ) {
						$label    = $options['label'] ?? $field;
						$facets[] = array(
							'value' => $field,
							'label' => $label,
						);
					}
				}
			}
			$facet_values = array_map(
				static function ( $facet_row ) {
					return isset( $facet_row['value'] ) ? (string) $facet_row['value'] : '';
				},
				$facets
			);
			if ( in_array( 'post_date', $facet_values, true ) || in_array( 'post_date_ts', $facet_values, true ) ) {
				$facets[] = array(
					'value' => 'post_year',
					'label' => __( 'Year', 'wlc-search-integration-with-algolia' ),
				);
			}

			// Handle taxonomies special case.
			// Taxonomies are indexed as flat arrays: taxonomies_slugs.{taxonomy}[] and taxonomies_names.{taxonomy}[].
			// For editor UX, expose taxonomy "name" facets by default.
			if ( isset( $selected_fields['taxonomies'] ) && ! empty( $selected_fields['taxonomies']['facet'] ) && '1' === $selected_fields['taxonomies']['facet'] ) {
				$taxonomies = get_object_taxonomies( $post_type, 'objects' );
				if ( is_array( $taxonomies ) ) {
					foreach ( $taxonomies as $taxonomy ) {
						if ( isset( $taxonomy->name ) && '' !== (string) $taxonomy->name ) {
							$tax_label = $taxonomy->labels->singular_name ?? $taxonomy->name;
							$facets[]  = array(
								'value' => 'taxonomies.taxonomies_names.' . (string) $taxonomy->name,
								'label' => $tax_label,
							);
						}
					}
				}
			}

			if ( 'product' === $post_type ) {
				$facets[] = array(
					'value' => 'rating_bucket',
					'label' => __( 'Rating', 'wlc-search-integration-with-algolia' ),
				);
			}
		}

		return rest_ensure_response( array( 'facets' => $facets ) );
	}

	/**
	 * Get index keys (fields) for a post type from Algolia.
	 *
	 * @param \WP_REST_Request $request Request object.
	 * @return \WP_Error|\WP_HTTP_Response|\WP_REST_Response
	 */
	public static function get_index_keys( $request ) {
		$post_type_param = $request->get_param( 'post_type' );
		$post_type       = sanitize_key( $post_type_param ? $post_type_param : 'post' );
		$keys            = array();

		try {
			require_once plugin_dir_path( __DIR__ ) . 'classes/class-algolia-settings-options.php';
			require_once plugin_dir_path( __DIR__ ) . '../vendor/autoload.php';

			$settings       = new Algolia_Settings_Options();
			$app_id         = $settings->get_application_id();
			$search_api_key = $settings->get_search_api_key();
			$base_index     = $settings->get_index_name();

			if ( empty( $app_id ) || empty( $search_api_key ) || empty( $base_index ) ) {
				return rest_ensure_response( array( 'keys' => array() ) );
			}

			$index_name = $base_index . '_' . $post_type;
			$client     = \Algolia\AlgoliaSearch\Api\SearchClient::create( $app_id, $search_api_key );

			if ( ! $client ) {
				return rest_ensure_response( array( 'keys' => array() ) );
			}

			// Get one hit to extract keys. The v4 client returns an associative array.
			$response = $client->searchSingleIndex(
				$index_name,
				array(
					'query'       => '',
					'hitsPerPage' => 1,
				)
			);

			if ( isset( $response['hits'] ) && ! empty( $response['hits'] ) ) {
				$hit  = $response['hits'][0];
				$keys = self::flatten_object_keys( $hit, '' );
			}
		} catch ( \Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch -- Intentional: fail silently, return empty array.
		}

		return rest_ensure_response( array( 'keys' => $keys ) );
	}

	/**
	 * Resolve canonical permalink by WordPress post ID.
	 *
	 * @param \WP_REST_Request $request Request object.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public static function get_permalink_by_post_id( $request ) {
		$post_id = absint( $request->get_param( 'post_id' ) );
		if ( $post_id <= 0 ) {
			return new \WP_Error( 'wlc_algolia_invalid_post_id', 'Invalid post_id', array( 'status' => 400 ) );
		}

		// Endpoint is public, so only resolve permalinks for posts that any
		// visitor could see anyway (published, publicly viewable post type).
		if ( ! is_post_publicly_viewable( $post_id ) ) {
			return new \WP_Error( 'wlc_algolia_permalink_not_found', 'Permalink not found', array( 'status' => 404 ) );
		}

		$permalink = get_permalink( $post_id );
		if ( empty( $permalink ) ) {
			return new \WP_Error( 'wlc_algolia_permalink_not_found', 'Permalink not found', array( 'status' => 404 ) );
		}

		return rest_ensure_response(
			array(
				'post_id'   => $post_id,
				'permalink' => esc_url_raw( $permalink ),
			)
		);
	}

	/**
	 * Flatten object keys recursively.
	 *
	 * @param mixed  $obj Object or array to flatten.
	 * @param string $prefix Prefix for keys.
	 * @return array Array of { path, sample } objects.
	 */
	private static function flatten_object_keys( $obj, $prefix = '' ) {
		$result = array();
		if ( ! is_array( $obj ) && ! is_object( $obj ) ) {
			return array();
		}

		foreach ( $obj as $key => $value ) {
			$path = $prefix ? $prefix . '.' . $key : $key;

			if ( is_array( $value ) || is_object( $value ) ) {
				if ( isset( $value[0] ) && is_numeric( key( $value ) ) ) {
					// Array - take first element if available.
					if ( ! empty( $value[0] ) && ( is_array( $value[0] ) || is_object( $value[0] ) ) ) {
						$result = array_merge( $result, self::flatten_object_keys( $value[0], $path ) );
					} else {
						$sample   = is_scalar( $value[0] ) ? $value[0] : null;
						$result[] = array(
							'path'   => $path,
							'sample' => $sample,
						);
					}
				} else {
					// Object - recurse.
					$result = array_merge( $result, self::flatten_object_keys( $value, $path ) );
				}
			} else {
				$result[] = array(
					'path'   => $path,
					'sample' => is_scalar( $value ) ? $value : null,
				);
			}
		}

		return $result;
	}
}
