<?php
/**
 * Keep Algolia indices in sync when posts change (no manual reindex for every edit).
 *
 * @package WLC_Algolia
 */

namespace wlc_algolia;

defined( 'ABSPATH' ) || die( 'Direct file access is forbidden' );

/**
 * Hooks save/delete to upsert or remove records per enabled post type.
 */
class Algolia_Sync_Posts extends Client_Helper {

	/**
	 * Register WordPress hooks.
	 *
	 * @return void
	 */
	public function hooks(): void {
		// After wp_insert_post completes (incl. WooCommerce product saves) so DB state matches the record we send.
		add_action( 'wp_after_insert_post', array( $this, 'sync_after_insert' ), 100, 4 );
		add_action( 'before_delete_post', array( $this, 'sync_on_before_delete' ), 20, 2 );
		if ( class_exists( '\WooCommerce' ) ) {
			add_action( 'woocommerce_before_delete_product', array( $this, 'sync_wc_before_delete_product' ), 10, 2 );
		}
	}

	/**
	 * Prefer wp_after_insert_post (WP 5.6+): runs when the post row is fully updated.
	 *
	 * @param int           $post_id     Post ID.
	 * @param \WP_Post      $post        Post object.
	 * @param bool          $update      True if updated.
	 * @param \WP_Post|null $post_before Pre-update post (insert = null).
	 * @return void
	 */
	public function sync_after_insert( $post_id, $post, $update = false, $post_before = null ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
		unset( $post_before, $update );
		$this->run_sync_for_post( (int) $post_id, $post, 'insert' );
	}

	/**
	 * Validate and sync one post to Algolia.
	 *
	 * @param int             $post_id Post ID.
	 * @param \WP_Post|object $post    Post object.
	 * @param string          $context Debug context.
	 * @return void
	 */
	private function run_sync_for_post( int $post_id, $post, string $context = '' ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
		unset( $context );

		if ( ! $post instanceof \WP_Post ) {
			return;
		}

		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}

		if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
			return;
		}

		if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
			return;
		}

		if ( ! $this->wlc_check_algolia_api_credentials() ) {
			return;
		}

		clean_post_cache( $post_id );
		$fresh = get_post( $post_id );
		if ( ! $fresh instanceof \WP_Post ) {
			return;
		}
		$post = $fresh;

		$post_type = $post->post_type;
		if ( ! $this->is_post_type_index_enabled( $post_type ) ) {
			return;
		}

		$index_name = $this->get_resolved_index_name( $post_type );
		if ( '' === $index_name ) {
			return;
		}

		if ( ! $this->is_post_searchable_in_index( $post ) ) {
			$this->algolia_client->delete_object( $index_name, (string) $post_id );
			return;
		}

		$data = $this->build_payload_for_post( $post );

		// Do not push facet settings on every save (expensive); full reindex from admin updates those.
		$this->algolia_client->index_post( $data, $index_name, 1, array(), '' );
	}

	/**
	 * WooCommerce removes the product post after this hook; ensure Algolia delete runs even if WP's hook order varies.
	 *
	 * @param int              $product_id Product (post) ID.
	 * @param \WC_Product|null $product    Product object (not always passed).
	 * @return void
	 */
	public function sync_wc_before_delete_product( $product_id, $product = null ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
		unset( $product );
		$pid = (int) $product_id;
		if ( $pid <= 0 ) {
			return;
		}
		$wp_post = get_post( $pid );
		if ( ! $wp_post instanceof \WP_Post || 'product' !== $wp_post->post_type ) {
			return;
		}
		$this->remove_post_from_algolia( $pid, $wp_post );
	}

	/**
	 * Permanently removing a post: ensure Algolia has no stale object.
	 *
	 * @param int      $post_id Post ID.
	 * @param \WP_Post $post    Post object.
	 * @return void
	 */
	public function sync_on_before_delete( $post_id, $post ): void {
		$post_id = (int) $post_id;
		if ( $post_id <= 0 ) {
			return;
		}

		if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
			return;
		}

		if ( wp_is_post_revision( $post_id ) ) {
			return;
		}

		if ( ! $post instanceof \WP_Post ) {
			$post = get_post( $post_id );
		}
		if ( ! $post instanceof \WP_Post ) {
			return;
		}

		$this->remove_post_from_algolia( $post_id, $post );
	}

	/**
	 * Delete one indexed object for this WordPress post (all known objectID variants are handled in the client).
	 *
	 * @param int      $post_id Post ID.
	 * @param \WP_Post $post    Post object.
	 * @return void
	 */
	private function remove_post_from_algolia( int $post_id, \WP_Post $post ): void {
		if ( ! $this->wlc_check_algolia_api_credentials() ) {
			return;
		}

		$post_type = $post->post_type;
		if ( ! $this->is_post_type_index_enabled( $post_type ) ) {
			return;
		}

		$index_name = $this->get_resolved_index_name( $post_type );
		if ( '' === $index_name ) {
			return;
		}

		$this->algolia_client->delete_object( $index_name, (string) $post_id );
	}

	/**
	 * Index name for a post type: optional full override from Post type index → Custom index name.
	 *
	 * @param string $post_type Post type slug.
	 * @return string Empty if neither custom nor base prefix is configured.
	 */
	private function get_resolved_index_name( string $post_type ): string {
		$post_type = sanitize_key( $post_type );
		if ( '' === $post_type ) {
			return '';
		}

		$map = get_option( 'wlc_algolia_settings_post_type_index', array() );
		if ( is_array( $map ) ) {
			$custom = isset( $map[ $post_type . '_custom_index' ] ) ? trim( (string) $map[ $post_type . '_custom_index' ] ) : '';
			if ( '' !== $custom ) {
				return $custom;
			}
		}

		$base = (string) $this->algolia_settings->get_index_name();
		if ( '' === $base ) {
			return '';
		}

		return $base . '_' . $post_type;
	}

	/**
	 * Post types the user enabled on the "Post type index" settings screen.
	 *
	 * @param string $post_type Post type slug.
	 * @return bool
	 */
	private function is_post_type_index_enabled( string $post_type ): bool {
		$post_type = sanitize_key( $post_type );
		if ( '' === $post_type ) {
			return false;
		}

		$map = get_option( 'wlc_algolia_settings_post_type_index', array() );
		if ( ! is_array( $map ) ) {
			return false;
		}

		return '1' === (string) ( $map[ $post_type ] ?? '0' );
	}

	/**
	 * Mirror bulk indexing: same statuses as {@see Algolia_Index_Helper::wlc_prepare_args()}.
	 *
	 * @param \WP_Post $post Post.
	 * @return bool
	 */
	private function is_post_searchable_in_index( \WP_Post $post ): bool {
		if ( 'attachment' === $post->post_type ) {
			return 'inherit' === $post->post_status;
		}

		return 'publish' === $post->post_status;
	}

	/**
	 * Build payload like batch indexing (CPT field map + defaults).
	 *
	 * @param \WP_Post $post Post.
	 * @return array
	 */
	private function build_payload_for_post( \WP_Post $post ): array {
		$post_type = $post->post_type;

		$cpt_settings = get_option( 'wlc_algolia_settings_cpt_' . sanitize_key( $post_type ), array() );
		$selected     = array();
		if ( is_array( $cpt_settings ) ) {
			$selected = Algolia_Index_Helper::wlc_prepare_selected_fields( $cpt_settings, false );
		}

		if ( empty( $selected ) ) {
			$defaults = Algolia_Index_Helper::wlc_get_default_index_fields();
			$selected = $defaults[ $post_type ] ?? array();
		}

		if ( ! empty( $selected ) ) {
			return $this->algolia_client->payload_data( $post, true, $selected );
		}

		return $this->algolia_client->payload_data( $post, false, array() );
	}
}
