<?php
/**
 * Contains the Admin_Hooks class
 *
 * @package Crowdsignal_Forms\Admin
 */

namespace Crowdsignal_Forms\Admin;

use Crowdsignal_Forms\Admin\Crowdsignal_Forms_Admin;
use Crowdsignal_Forms\Admin\Crowdsignal_Forms_Admin_Notices;
use Crowdsignal_Forms\Models\Poll;
use Crowdsignal_Forms\Crowdsignal_Forms;
use Crowdsignal_Forms\Gateways\Post_Survey_Meta_Gateway;
use Crowdsignal_Forms\Auth\Crowdsignal_Forms_Api_Authenticator;
use Crowdsignal_Forms\Synchronization\Post_Sync_Entity;
use Crowdsignal_Forms\Synchronization\Comment_Sync_Entity;
use Crowdsignal_Forms\Synchronization\Poll_Block_Synchronizer;
use Crowdsignal_Forms\Synchronization\Survey_Block_Synchronizer;

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

/**
 * Admin_Hooks
 */
class Admin_Hooks {
	/**
	 * The poll ids meta key.
	 */
	const CROWDSIGNAL_FORMS_POLL_IDS = '_crowdsignal_forms_poll_ids';

	/**
	 * The survey block names that use surveyId/surveyClientId.
	 *
	 * @var array
	 */
	public static $survey_block_names = array(
		'crowdsignal-forms/nps',
		'crowdsignal-forms/feedback',
	);

	/**
	 * Is the class hooked.
	 *
	 * @var bool Is the class hooked.
	 */
	private $is_hooked = false;

	/**
	 * The admin page.
	 *
	 * @var Crowdsignal_Forms_Admin
	 */
	private $admin;

	/**
	 * Perform any hooks.
	 *
	 * @since 0.9.0
	 *
	 * @return $this
	 */
	public function hook() {
		if ( $this->is_hooked ) {
			return $this;
		}
		$this->is_hooked = true;
		// Do any hooks required.
		if ( is_admin() && apply_filters( 'crowdsignal_forms_show_admin', true ) ) {
			$this->admin = new Crowdsignal_Forms_Admin();

			// admin page.
			add_action( 'admin_init', array( $this->admin, 'admin_init' ) );
			add_action( 'admin_menu', array( $this->admin, 'admin_menu' ), 12 );
			add_action( 'admin_enqueue_scripts', array( $this->admin, 'admin_enqueue_scripts' ) );

			// admin notices.
			add_action( 'crowdsignal_forms_init_admin_notices', 'Crowdsignal_Forms\Admin\Crowdsignal_Forms_Admin_Notices::init_core_notices' );
			add_action( 'admin_notices', 'Crowdsignal_Forms\Admin\Crowdsignal_Forms_Admin_Notices::display_notices' );
			add_action( 'wp_loaded', 'Crowdsignal_Forms\Admin\Crowdsignal_Forms_Admin_Notices::dismiss_notices' );
		}

		add_filter( 'wp_insert_post_data', array( $this, 'guard_survey_ids_in_content' ), 10, 2 );

		add_action( 'rest_api_init', array( $this, 'register_survey_meta_rest_hooks' ) );

		if ( is_admin() ) {
			add_action( 'load-post.php', array( $this, 'ensure_survey_meta_on_edit_screen' ) );
		}

		add_action( 'save_post', array( $this, 'save_polls_to_api' ), 10, 3 );
		add_action( 'save_post', array( $this, 'save_surveys_to_api' ), 10, 3 );
		/**
		 * Should we synchronize poll blocks in comments too?
		 *
		 * @since 1.0.0
		 *
		 * @param bool $should_sync Synchronize the poll blocks in comments.
		 * @return bool
		 */
		$should_sync_comment_polls = (bool) apply_filters( 'crowdsignal_forms_should_sync_comment_polls', true );
		if ( $should_sync_comment_polls ) {
			add_action( 'comment_post', array( $this, 'save_polls_to_api_from_new_comment' ), 10, 3 );
			add_action( 'edit_comment', array( $this, 'save_polls_to_api_from_updated_comment' ), 10, 2 );
		}

		return $this;
	}

	/**
	 * Save polls in new comments.
	 *
	 * @param  int        $comment_id       The comment id.
	 * @param int|string $comment_approved Comment approved status.
	 * @param array      $commentdata      The comment data.
	 *
	 * @return void|bool
	 *
	 * @throws \Exception In case of bad request. This is temporary.
	 *
	 * @since 1.0.0
	 */
	public function save_polls_to_api_from_new_comment( $comment_id, $comment_approved, $commentdata ) {
		$saver        = new Comment_Sync_Entity( $comment_id, $comment_approved, $commentdata );
		$synchronizer = new Poll_Block_Synchronizer( $saver );
		return $synchronizer->synchronize();
	}

	/**
	 * Save polls in updated comments.
	 *
	 * @param int   $comment_id  The comment id.
	 * @param array $commentdata The comment data.
	 *
	 * @return void|bool
	 *
	 * @throws \Exception In case of bad request. This is temporary.
	 *
	 * @since 1.0.0
	 */
	public function save_polls_to_api_from_updated_comment( $comment_id, $commentdata ) {
		$saver        = new Comment_Sync_Entity( $comment_id, null, $commentdata );
		$synchronizer = new Poll_Block_Synchronizer( $saver );
		return $synchronizer->synchronize();
	}

	/**
	 * Will save any pending polls.
	 *
	 * @param int      $post_ID The id.
	 * @param \WP_Post $post The post.
	 * @param bool     $is_update Is this an update.
	 *
	 * @since 0.9.0
	 * @return void|bool
	 *
	 * @throws \Exception In case of bad request. This is temporary.
	 */
	public function save_polls_to_api( $post_ID, $post, $is_update = false ) {
		$saver        = new Post_Sync_Entity( $post_ID, $post, $is_update );
		$synchronizer = new Poll_Block_Synchronizer( $saver );
		return $synchronizer->synchronize();
	}

	/**
	 * Archive polls with these ids.
	 *
	 * @param array $poll_ids_to_archive Ids to archive.
	 */
	private function archive_polls_with_ids( $poll_ids_to_archive ) {
		if ( empty( $poll_ids_to_archive ) ) {
			return;
		}
		$gateway = Crowdsignal_Forms::instance()->get_api_gateway();
		foreach ( $poll_ids_to_archive as $id_to_archive ) {
			$gateway->archive_poll( $id_to_archive );
		}
	}

	/**
	 * Will save any pending surveys (NPS and Feedback).
	 *
	 * @param int      $post_ID   The post id.
	 * @param \WP_Post $post      The post.
	 * @param bool     $is_update Is this an update.
	 *
	 * @since 1.8.0
	 * @return void|bool
	 *
	 * @throws \Exception In case of bad request.
	 */
	public function save_surveys_to_api( $post_ID, $post, $is_update = false ) {
		$entity       = new Post_Sync_Entity( $post_ID, $post, $is_update );
		$synchronizer = new Survey_Block_Synchronizer( $entity );
		return $synchronizer->synchronize();
	}

	/**
	 * Guard against surveyId injection in post content.
	 *
	 * Prevents bare `surveyId` attributes from being introduced into post
	 * content through any save vector (editor, XML-RPC, REST API, wp_update_post).
	 * A bare `surveyId` in new content is only allowed if it existed in the old
	 * content OR has matching `_cs_survey_*` meta on this post.
	 *
	 * @param array $data    An array of slashed, sanitized post data.
	 * @param array $postarr An array of sanitized (and slashed) but otherwise unmodified post data.
	 * @return array Modified post data.
	 */
	public function guard_survey_ids_in_content( $data, $postarr ) {
		$content = wp_unslash( $data['post_content'] );

		$blocks         = array();
		$new_survey_ids = self::get_legacy_survey_ids_from_content( $content, $blocks );

		if ( empty( $new_survey_ids ) ) {
			return $data;
		}

		$post_id = ! empty( $postarr['ID'] ) ? (int) $postarr['ID'] : 0;

		// Get old content for comparison.
		$old_content    = $post_id ? (string) get_post_field( 'post_content', $post_id ) : '';
		$old_survey_ids = self::get_legacy_survey_ids_from_content( $old_content );

		$meta_gateway = Crowdsignal_Forms::instance()->get_post_survey_meta_gateway();
		$modified     = false;

		self::guard_blocks( $blocks, $old_survey_ids, $post_id, $meta_gateway, $modified );

		if ( $modified ) {
			$data['post_content'] = wp_slash( serialize_blocks( $blocks ) );
		}

		return $data;
	}

	/**
	 * Recursively guard blocks against surveyId injection.
	 *
	 * @param array                  $blocks        Blocks array (modified by reference).
	 * @param array                  $old_survey_ids Survey IDs present in old content.
	 * @param int                    $post_id       The post ID (0 for new posts).
	 * @param Post_Survey_Meta_Gateway $meta_gateway  The meta gateway.
	 * @param bool                   $modified      Whether any blocks were modified (by reference).
	 */
	private static function guard_blocks( &$blocks, $old_survey_ids, $post_id, $meta_gateway, &$modified ) {
		foreach ( $blocks as &$block ) {
			if (
				in_array( $block['blockName'], self::$survey_block_names, true )
				&& ! empty( $block['attrs']['surveyId'] )
				&& empty( $block['attrs']['surveyClientId'] )
			) {
				$survey_id = (int) $block['attrs']['surveyId'];

				// Allow if pre-existing in old content.
				if ( in_array( $survey_id, $old_survey_ids, true ) ) {
					continue;
				}

				// Allow if this post has matching meta.
				if ( $post_id && null !== $meta_gateway->get_client_id_for_survey_id( $post_id, $survey_id ) ) {
					continue;
				}

				// Unknown surveyId — replace with a fresh surveyClientId.
				unset( $block['attrs']['surveyId'] );
				$block['attrs']['surveyClientId'] = wp_generate_uuid4();
				$modified                         = true;
			}

			if ( ! empty( $block['innerBlocks'] ) ) {
				self::guard_blocks( $block['innerBlocks'], $old_survey_ids, $post_id, $meta_gateway, $modified );
			}
		}
	}

	/**
	 * Register REST API hooks for survey meta generation on edit.
	 *
	 * @return void
	 */
	public function register_survey_meta_rest_hooks() {
		$post_types = get_post_types( array( 'show_in_rest' => true ) );

		foreach ( $post_types as $post_type ) {
			add_filter( "rest_prepare_{$post_type}", array( $this, 'ensure_survey_meta_for_legacy_blocks_rest' ), 10, 3 );
		}
	}

	/**
	 * Generate `_cs_survey_{uuid}` meta for legacy survey blocks when a post is loaded for editing via REST API.
	 *
	 * @param \WP_REST_Response $response The response object.
	 * @param \WP_Post          $post     The post object.
	 * @param \WP_REST_Request  $request  The request object.
	 * @return \WP_REST_Response
	 */
	public function ensure_survey_meta_for_legacy_blocks_rest( $response, $post, $request ) {
		if ( 'edit' !== $request['context'] ) {
			return $response;
		}

		if ( ! current_user_can( 'edit_post', $post->ID ) ) {
			return $response;
		}

		$this->ensure_survey_meta_for_post( $post->ID, $post->post_content );

		return $response;
	}

	/**
	 * Generate `_cs_survey_{uuid}` meta for legacy survey blocks when the classic editor loads.
	 *
	 * @return void
	 */
	public function ensure_survey_meta_on_edit_screen() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! isset( $_GET['post'] ) ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$post_id = (int) $_GET['post'];

		if ( ! $post_id ) {
			return;
		}

		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}

		$post = get_post( $post_id );

		if ( ! $post ) {
			return;
		}

		$this->ensure_survey_meta_for_post( $post->ID, $post->post_content );
	}

	/**
	 * Ensure meta entries exist for all legacy survey blocks in a post's content.
	 *
	 * @param int    $post_id The post ID.
	 * @param string $content The post content.
	 * @return void
	 */
	private function ensure_survey_meta_for_post( $post_id, $content ) {
		$survey_ids = self::get_legacy_survey_ids_from_content( $content );

		if ( empty( $survey_ids ) ) {
			return;
		}

		$meta_gateway         = Crowdsignal_Forms::instance()->get_post_survey_meta_gateway();
		$tracking_survey_ids  = get_post_meta( $post_id, Survey_Block_Synchronizer::CROWDSIGNAL_FORMS_SURVEY_IDS, true );
		$tracking_survey_ids  = is_array( $tracking_survey_ids ) ? $tracking_survey_ids : array();
		$tracking_changed     = false;

		foreach ( $survey_ids as $survey_id ) {
			$meta_gateway->ensure_meta_for_survey_id( $post_id, $survey_id );

			if ( ! in_array( $survey_id, $tracking_survey_ids, true ) ) {
				$tracking_survey_ids[] = $survey_id;
				$tracking_changed      = true;
			}
		}

		if ( $tracking_changed ) {
			update_post_meta( $post_id, Survey_Block_Synchronizer::CROWDSIGNAL_FORMS_SURVEY_IDS, $tracking_survey_ids );
		}
	}

	/**
	 * Parse post content and return survey IDs from legacy blocks.
	 *
	 * A legacy block has `surveyId` in its attributes but no `surveyClientId`.
	 *
	 * @param string $content The post content.
	 * @param array  $blocks  Optional. Populated with parsed blocks by reference.
	 * @return array Array of integer survey IDs.
	 */
	public static function get_legacy_survey_ids_from_content( $content, &$blocks = null ) {
		$blocks = array();

		if ( empty( $content ) ) {
			return array();
		}

		// Cheap check: skip block parsing for content without survey blocks.
		$has_survey_block = false;
		foreach ( self::$survey_block_names as $block_name ) {
			if ( false !== strpos( $content, $block_name ) ) {
				$has_survey_block = true;
				break;
			}
		}

		if ( ! $has_survey_block ) {
			return array();
		}

		$blocks     = parse_blocks( $content );
		$survey_ids = array();

		self::collect_legacy_survey_ids( $blocks, $survey_ids );

		return $survey_ids;
	}

	/**
	 * Recursively collect legacy survey IDs from blocks.
	 *
	 * @param array $blocks     The blocks to search.
	 * @param array $survey_ids Collects survey IDs (modified by reference).
	 */
	private static function collect_legacy_survey_ids( $blocks, &$survey_ids ) {
		foreach ( $blocks as $block ) {
			if (
				in_array( $block['blockName'], self::$survey_block_names, true )
				&& ! empty( $block['attrs']['surveyId'] )
				&& empty( $block['attrs']['surveyClientId'] )
			) {
				$survey_ids[] = (int) $block['attrs']['surveyId'];
			}

			if ( ! empty( $block['innerBlocks'] ) ) {
				self::collect_legacy_survey_ids( $block['innerBlocks'], $survey_ids );
			}
		}
	}
}