<?php
/**
 * Changeloger REST API
 *
 * Core REST API endpoints for Releases and Products (Free Plugin)
 *
 * @package Changeloger
 */

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

/**
 * Class Changeloger_REST_API
 */
class Changeloger_REST_API {

	/**
	 * Initialize the class
	 *
	 * @return void
	 */
	public static function init(): void {
		new self();
	}

	/**
	 * Constructor
	 *
	 * @return void
	 */
	public function __construct() {
		add_action( 'rest_api_init', [ $this, 'register_routes' ] );

		// Feedback cache invalidation hooks.
		add_action( 'save_post_' . Changeloger_Post_Types::POST_TYPE_FEEDBACK, [ $this, 'invalidate_feedback_cache' ] );
		add_action( 'cha_feedback_created', [ $this, 'invalidate_feedback_cache' ] );
		add_action( 'cha_feedback_status_changed', [ $this, 'invalidate_feedback_cache' ] );
	}

	/**
	 * Register REST API routes
	 *
	 * @return void
	 */
	public function register_routes(): void {
		// ========== Utility Endpoints ==========
		register_rest_route( 'changeloger/v1', '/fetch-txt', [
			'methods'             => 'GET',
			'callback'            => [ $this, 'fetch_txt_callback' ],
			'permission_callback' => '__return_true',
		] );

		// Preview URL Endpoint
		register_rest_route( 'changeloger/v1', '/preview-url', [
			'methods'             => WP_REST_Server::READABLE,
			'callback'            => [ $this, 'get_preview_url' ],
			'permission_callback' => [ $this, 'check_manage_options' ],
			'args'                => [
				'product_id' => [
					'default'           => 0,
					'sanitize_callback' => 'absint',
				],
			],
		] );

		// Create Release Page Endpoint
		register_rest_route( 'changeloger/v1', '/create-release-page', [
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => [ $this, 'create_release_page' ],
			'permission_callback' => [ $this, 'check_manage_options' ],
			'args'                => [
				'product_id' => [
					'required'          => true,
					'sanitize_callback' => 'absint',
				],
				'tab' => [
					'default'           => 'releases',
					'sanitize_callback' => 'sanitize_text_field',
				],
			],
		] );

		// ========== Releases API Endpoints ==========
		register_rest_route( 'changeloger/v1', '/releases', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_releases' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
				'args'                => [
					'status' => [
						'default'           => 'any',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'search' => [
						'default'           => '',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'product' => [
						'default'           => '',
						'sanitize_callback' => 'absint',
					],
					'per_page' => [
						'default'           => 20,
						'sanitize_callback' => 'absint',
					],
					'page' => [
						'default'           => 1,
						'sanitize_callback' => 'absint',
					],
				],
			],
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'create_release' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
		] );

		register_rest_route( 'changeloger/v1', '/releases/(?P<id>\d+)', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_release' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
			[
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => [ $this, 'update_release' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
			[
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => [ $this, 'delete_release' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
		] );

		register_rest_route( 'changeloger/v1', '/releases/stats', [
			'methods'             => WP_REST_Server::READABLE,
			'callback'            => [ $this, 'get_releases_stats' ],
			'permission_callback' => [ $this, 'check_manage_options' ],
			'args'                => [
				'product' => [
					'default'           => '',
					'sanitize_callback' => 'absint',
				],
			],
		] );

		// ========== Products API Endpoints ==========
		register_rest_route( 'changeloger/v1', '/products', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_products' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'create_product' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
		] );

		register_rest_route( 'changeloger/v1', '/products/(?P<id>\d+)', [
			[
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => [ $this, 'update_product' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
			[
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => [ $this, 'delete_product' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
		] );

		// Demo Import Endpoint
		register_rest_route( 'changeloger/v1', '/import-sample', [
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => function() {
				if ( class_exists( 'Changeloger_Demo_Importer' ) ) {
					return Changeloger_Demo_Importer::import_demo_data();
				}
				return new WP_Error( 'class_missing', 'Importer class missing', [ 'status' => 500 ] );
			},
			'permission_callback' => [ $this, 'check_manage_options' ],
		] );

		// ========== Badges API Endpoints ==========
		register_rest_route( 'changeloger/v1', '/badges', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_badges' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'create_badge' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
		] );

		register_rest_route( 'changeloger/v1', '/badges/(?P<id>\d+)', [
			[
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => [ $this, 'update_badge' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
			[
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => [ $this, 'delete_badge' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
		] );

		// ========== Feedback API Endpoints ==========
		register_rest_route( 'changeloger/v1', '/feedback', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_feedback_list' ],
				'permission_callback' => '__return_true',
			],
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'create_feedback' ],
				'permission_callback' => [ $this, 'check_logged_in' ],
			],
		] );

		register_rest_route( 'changeloger/v1', '/feedback/(?P<id>\d+)', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_feedback' ],
				'permission_callback' => '__return_true',
			],
			[
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => [ $this, 'update_feedback' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
			[
				'methods'             => WP_REST_Server::DELETABLE,
				'callback'            => [ $this, 'delete_feedback' ],
				'permission_callback' => [ $this, 'check_manage_options' ],
			],
		] );

		register_rest_route( 'changeloger/v1', '/feedback/(?P<id>\d+)/upvote', [
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => [ $this, 'toggle_feedback_upvote' ],
			'permission_callback' => '__return_true',
		] );

		register_rest_route( 'changeloger/v1', '/feedback/(?P<id>\d+)/downvote', [
			'methods'             => WP_REST_Server::CREATABLE,
			'callback'            => [ $this, 'toggle_feedback_downvote' ],
			'permission_callback' => '__return_true',
		] );

		register_rest_route( 'changeloger/v1', '/feedback/(?P<id>\d+)/comments', [
			[
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => [ $this, 'get_feedback_comments' ],
				'permission_callback' => '__return_true',
			],
			[
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => [ $this, 'add_feedback_comment' ],
				'permission_callback' => [ $this, 'check_logged_in' ],
			],
		] );

		register_rest_route( 'changeloger/v1', '/feedback/stats', [
			'methods'             => WP_REST_Server::READABLE,
			'callback'            => [ $this, 'get_feedback_stats' ],
			'permission_callback' => [ $this, 'check_manage_options' ],
		] );
	}

	// ========== Permission Callbacks ==========

	/**
	 * Check manage_options capability
	 *
	 * @param WP_REST_Request|null $request Request object.
	 * @return bool
	 */
	public function check_manage_options( $request = null ): bool {
		if ( current_user_can( 'manage_options' ) ) {
			return true;
		}

		if ( $request ) {
			$nonce = $request->get_header( 'X-WP-Nonce' );
			if ( $nonce && wp_verify_nonce( $nonce, 'wp_rest' ) ) {
				wp_set_current_user( get_current_user_id() );
				return current_user_can( 'manage_options' );
			}
		}

		return false;
	}

	/**
	 * Check if user is logged in
	 *
	 * @param WP_REST_Request|null $request Request object.
	 * @return bool
	 */
	public function check_logged_in( $request = null ): bool {
		if ( is_user_logged_in() ) {
			return true;
		}

		if ( $request ) {
			$nonce = $request->get_header( 'X-WP-Nonce' );
			if ( $nonce && wp_verify_nonce( $nonce, 'wp_rest' ) ) {
				return get_current_user_id() > 0;
			}
		}

		return false;
	}

	// ========== Utility Callbacks ==========

	/**
	 * Fetch .txt file content from URL
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function fetch_txt_callback( $request ) {
		$url = $request->get_param( 'url' );

		if ( ! $url ) {
			return new WP_Error( 'no_url', 'No URL provided', [ 'status' => 400 ] );
		}

		$response = wp_remote_get( $url );

		if ( is_wp_error( $response ) ) {
			return new WP_Error( 'fetch_failed', 'Failed to fetch the file', [ 'status' => 500 ] );
		}

		$status = wp_remote_retrieve_response_code( $response );
		if ( $status !== 200 ) {
			return new WP_Error( 'not_found', 'URL returned ' . $status, [ 'status' => 404 ] );
		}

		$content_type = wp_remote_retrieve_header( $response, 'content-type' );

		if ( strpos( $content_type, 'text/plain' ) === false ) {
			return new WP_Error( 'invalid_type', 'This is not a .txt file', [ 'status' => 400 ] );
		}

		$body = wp_remote_retrieve_body( $response );

		if ( trim( $body ) === '' ) {
			return new WP_Error( 'empty_file', 'The file is empty', [ 'status' => 400 ] );
		}

		return rest_ensure_response( [ 'content' => $body ] );
	}

	/**
	 * Get preview URL for release-hub page
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_preview_url( WP_REST_Request $request ): WP_REST_Response {
		$product_id = $request->get_param( 'product_id' );
		$preview_url = Changeloger_Frontend::get_release_hub_page_url( $product_id );

		return rest_ensure_response( [
			'url'        => $preview_url,
			'product_id' => $product_id,
		] );
	}

	/**
	 * Create a page with release-hub block for a product
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function create_release_page( WP_REST_Request $request ) {
		$product_id = $request->get_param( 'product_id' );
		$tab        = $request->get_param( 'tab' );

		$product = get_term( $product_id, Changeloger_Post_Types::TAXONOMY_PRODUCT );
		if ( ! $product || is_wp_error( $product ) ) {
			return new WP_Error( 'invalid_product', __( 'Invalid product ID.', 'changeloger' ), [ 'status' => 400 ] );
		}

		$product_name = $product->name;

		$block_content = sprintf(
			'<!-- wp:cha/release-hub {"productId":%d} /-->',
			$product_id
		);

		$page_data = [
			'post_title'   => sprintf( __( '%s - Changelog', 'changeloger' ), $product_name ),
			'post_content' => $block_content,
			'post_status'  => 'publish',
			'post_type'    => 'page',
			'meta_input'   => [
				'_wp_page_template' => 'changelog-template.php',
			],
		];

		$page_id = wp_insert_post( $page_data, true );

		if ( is_wp_error( $page_id ) ) {
			return new WP_Error( 'create_failed', $page_id->get_error_message(), [ 'status' => 500 ] );
		}

		$page_url = get_permalink( $page_id );

		return rest_ensure_response( [
			'success'    => true,
			'page_id'    => $page_id,
			'page_url'   => $page_url . '#' . $tab,
			'product_id' => $product_id,
		] );
	}

	// ========== Releases API Callbacks ==========

	/**
	 * Get releases list
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_releases( WP_REST_Request $request ): WP_REST_Response {
		$status   = $request->get_param( 'status' );
		$search   = $request->get_param( 'search' );
		$product  = $request->get_param( 'product' );
		$per_page = $request->get_param( 'per_page' );
		$page     = $request->get_param( 'page' );

		$args = [
			'post_type'      => Changeloger_Post_Types::POST_TYPE,
			'posts_per_page' => $per_page,
			'paged'          => $page,
			'orderby'        => 'meta_value',
			'meta_key'       => Changeloger_Release_Meta::META_RELEASE_DATE,
			'order'          => 'DESC',
		];

		if ( $status && $status !== 'any' ) {
			$args['post_status'] = $status;
		} else {
			$args['post_status'] = [ 'publish', 'draft' ];
		}

		if ( $search ) {
			$args['s'] = $search;
		}

		if ( $product ) {
			$args['tax_query'] = [
				[
					'taxonomy' => Changeloger_Post_Types::TAXONOMY_PRODUCT,
					'field'    => 'term_id',
					'terms'    => $product,
				],
			];
		}

		$query = new WP_Query( $args );
		$releases = [];

		foreach ( $query->posts as $post ) {
			$releases[] = $this->format_release( $post );
		}

		return rest_ensure_response( [
			'releases'    => $releases,
			'total'       => $query->found_posts,
			'total_pages' => $query->max_num_pages,
			'page'        => $page,
		] );
	}

	/**
	 * Get single release
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_release( WP_REST_Request $request ) {
		$id   = $request->get_param( 'id' );
		$post = get_post( $id );

		if ( ! $post || $post->post_type !== Changeloger_Post_Types::POST_TYPE ) {
			return new WP_Error( 'not_found', __( 'Release not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		// Batch all meta queries into a single DB call for this post.
		update_meta_cache( 'post', [ $id ] );

		return rest_ensure_response( $this->format_release( $post ) );
	}

	/**
	 * Create release
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function create_release( WP_REST_Request $request ) {
		$data = $request->get_json_params();

		$post_data = [
			'post_type'    => Changeloger_Post_Types::POST_TYPE,
			'post_title'   => sanitize_text_field( $data['title'] ?? '' ),
			'post_content' => wp_kses_post( $data['notes'] ?? '' ),
			'post_status'  => sanitize_text_field( $data['status'] ?? 'draft' ),
		];

		$post_id = wp_insert_post( $post_data );

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

		// Save meta.
		if ( isset( $data['version'] ) ) {
			update_post_meta( $post_id, Changeloger_Release_Meta::META_VERSION, sanitize_text_field( $data['version'] ) );
		}
		if ( isset( $data['release_date'] ) ) {
			update_post_meta( $post_id, Changeloger_Release_Meta::META_RELEASE_DATE, sanitize_text_field( $data['release_date'] ) );
		}
		if ( isset( $data['change_items'] ) ) {
			update_post_meta( $post_id, Changeloger_Release_Meta::META_CHANGE_ITEMS, $this->sanitize_change_items( $data['change_items'] ) );
		}

		// Set product.
		if ( isset( $data['product'] ) && $data['product'] ) {
			wp_set_object_terms( $post_id, absint( $data['product'] ), Changeloger_Post_Types::TAXONOMY_PRODUCT );
		}

		// Set badge (meta - badge slug from CSF settings).
		if ( isset( $data['badge_id'] ) ) {
			$badge_slug = sanitize_text_field( $data['badge_id'] );
			update_post_meta( $post_id, '_cha_badge_slug', $badge_slug );
		}

		// Set featured image.
		if ( isset( $data['featured_image'] ) ) {
			$image_id = absint( $data['featured_image'] );
			if ( $image_id ) {
				update_post_meta( $post_id, Changeloger_Release_Meta::META_FEATURED_IMAGE, $image_id );
			} else {
				delete_post_meta( $post_id, Changeloger_Release_Meta::META_FEATURED_IMAGE );
			}
		}

		// Fire custom action after all meta and terms are saved.
		$post = get_post( $post_id );
		if ( 'publish' === $post->post_status ) {
			/**
			 * Fires after a release is published and all meta/terms are saved.
			 *
			 * @param int $post_id The release post ID.
			 */
			do_action( 'cha_release_published', $post_id );
		}

		return rest_ensure_response( $this->format_release( $post ) );
	}

	/**
	 * Update release
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_release( WP_REST_Request $request ) {
		$id   = $request->get_param( 'id' );
		$data = $request->get_json_params();
		$post = get_post( $id );

		if ( ! $post || $post->post_type !== Changeloger_Post_Types::POST_TYPE ) {
			return new WP_Error( 'not_found', __( 'Release not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		// Remember old status to detect publish transitions.
		$old_status = $post->post_status;

		$post_data = [ 'ID' => $id ];

		if ( isset( $data['title'] ) ) {
			$post_data['post_title'] = sanitize_text_field( $data['title'] );
		}
		if ( isset( $data['notes'] ) ) {
			$post_data['post_content'] = wp_kses_post( $data['notes'] );
		}
		if ( isset( $data['status'] ) ) {
			$post_data['post_status'] = sanitize_text_field( $data['status'] );
		}

		wp_update_post( $post_data );

		// Update meta.
		if ( isset( $data['version'] ) ) {
			update_post_meta( $id, Changeloger_Release_Meta::META_VERSION, sanitize_text_field( $data['version'] ) );
		}
		if ( isset( $data['release_date'] ) ) {
			update_post_meta( $id, Changeloger_Release_Meta::META_RELEASE_DATE, sanitize_text_field( $data['release_date'] ) );
		}
		if ( isset( $data['change_items'] ) ) {
			update_post_meta( $id, Changeloger_Release_Meta::META_CHANGE_ITEMS, $this->sanitize_change_items( $data['change_items'] ) );
		}

		// Update product.
		if ( isset( $data['product'] ) ) {
			if ( $data['product'] ) {
				wp_set_object_terms( $id, absint( $data['product'] ), Changeloger_Post_Types::TAXONOMY_PRODUCT );
			} else {
				wp_set_object_terms( $id, [], Changeloger_Post_Types::TAXONOMY_PRODUCT );
			}
		}

		// Update badge (meta - badge slug from CSF settings).
		if ( isset( $data['badge_id'] ) ) {
			$badge_slug = sanitize_text_field( $data['badge_id'] );
			update_post_meta( $id, '_cha_badge_slug', $badge_slug );
		}

		// Update featured image.
		if ( isset( $data['featured_image'] ) ) {
			$image_id = absint( $data['featured_image'] );
			if ( $image_id ) {
				update_post_meta( $id, Changeloger_Release_Meta::META_FEATURED_IMAGE, $image_id );
			} else {
				delete_post_meta( $id, Changeloger_Release_Meta::META_FEATURED_IMAGE );
			}
		}

		$post = get_post( $id );

		// Fire custom action when status transitions to publish.
		$new_status = $post->post_status;
		if ( 'publish' === $new_status && 'publish' !== $old_status ) {
			/** This action is documented in create_release(). */
			do_action( 'cha_release_published', $id );
		}

		return rest_ensure_response( $this->format_release( $post ) );
	}

	/**
	 * Delete release
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_release( WP_REST_Request $request ) {
		$id   = $request->get_param( 'id' );
		$post = get_post( $id );

		if ( ! $post || $post->post_type !== Changeloger_Post_Types::POST_TYPE ) {
			return new WP_Error( 'not_found', __( 'Release not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		wp_delete_post( $id, true );

		return rest_ensure_response( [ 'deleted' => true, 'id' => $id ] );
	}

	/**
	 * Get stats
	 *
	 * @return WP_REST_Response
	 */
	public function get_releases_stats(): WP_REST_Response {
		$post_type = Changeloger_Post_Types::POST_TYPE;

		$total = wp_count_posts( $post_type );
		$total_count     = ( $total->publish ?? 0 ) + ( $total->draft ?? 0 );
		$published_count = $total->publish ?? 0;
		$draft_count     = $total->draft ?? 0;

		// Get latest version
		$latest_args = [
			'post_type'      => $post_type,
			'posts_per_page' => 1,
			'post_status'    => 'publish',
			'orderby'        => 'date',
			'order'          => 'DESC',
		];
		$latest_query   = new WP_Query( $latest_args );
		$latest_version = '—';
		$latest_product = '';

		if ( $latest_query->have_posts() ) {
			$latest_post    = $latest_query->posts[0];
			$latest_version = get_post_meta( $latest_post->ID, Changeloger_Release_Meta::META_VERSION, true ) ?: '—';

			$terms = wp_get_post_terms( $latest_post->ID, Changeloger_Post_Types::TAXONOMY_PRODUCT );
			if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
				$latest_product = $terms[0]->name;
			}
		}

		return rest_ensure_response( [
			'total'          => $total_count,
			'published'      => $published_count,
			'drafts'         => $draft_count,
			'latest_version' => $latest_version,
			'latest_product' => $latest_product,
		] );
	}

	/**
	 * Format release for response
	 *
	 * @param WP_Post $post Post object.
	 * @return array
	 */
	private function format_release( $post ): array {
		$terms = wp_get_post_terms( $post->ID, Changeloger_Post_Types::TAXONOMY_PRODUCT );
		$product = null;
		if ( ! empty( $terms ) && ! is_wp_error( $terms ) ) {
			$product = [
				'id'   => $terms[0]->term_id,
				'name' => $terms[0]->name,
				'slug' => $terms[0]->slug,
			];
		}

		// Get badge from post meta and lookup from CSF settings.
		$badge      = null;
		$badge_slug = get_post_meta( $post->ID, '_cha_badge_slug', true );
		if ( ! empty( $badge_slug ) ) {
			$badges = Changeloger_Post_Types::get_release_badges();
			foreach ( $badges as $b ) {
				if ( $b['slug'] === $badge_slug ) {
					$badge = $b;
					break;
				}
			}
		}

		// Get featured image.
		$featured_image    = null;
		$featured_image_id = get_post_meta( $post->ID, Changeloger_Release_Meta::META_FEATURED_IMAGE, true );
		if ( $featured_image_id ) {
			$image_url = wp_get_attachment_image_url( $featured_image_id, 'large' );
			if ( $image_url ) {
				$featured_image = [
					'id'  => (int) $featured_image_id,
					'url' => $image_url,
				];
			}
		}

		return [
			'id'             => $post->ID,
			'title'          => $post->post_title,
			'notes'          => $post->post_content,
			'status'         => $post->post_status,
			'version'        => get_post_meta( $post->ID, Changeloger_Release_Meta::META_VERSION, true ),
			'badge'          => $badge,
			'release_date'   => get_post_meta( $post->ID, Changeloger_Release_Meta::META_RELEASE_DATE, true ),
			'change_items'   => get_post_meta( $post->ID, Changeloger_Release_Meta::META_CHANGE_ITEMS, true ) ?: [],
			'featured_image' => $featured_image,
			'product'        => $product,
			'permalink'      => get_permalink( $post->ID ),
			'created_at'     => $post->post_date,
			'modified_at'    => $post->post_modified,
		];
	}

	/**
	 * Sanitize change items
	 *
	 * @param array $items Change items.
	 * @return array
	 */
	private function sanitize_change_items( $items ): array {
		if ( ! is_array( $items ) ) {
			return [];
		}

		$sanitized = [];
		foreach ( $items as $item ) {
			if ( isset( $item['group'] ) && isset( $item['items'] ) && is_array( $item['items'] ) ) {
				$sanitized[] = [
					'group' => sanitize_text_field( $item['group'] ),
					'items' => array_map( 'sanitize_text_field', $item['items'] ),
				];
			}
		}

		return $sanitized;
	}

	// ========== Products API Callbacks ==========

	/**
	 * Get products list
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_products( WP_REST_Request $request ): WP_REST_Response {
		$terms = get_terms( [
			'taxonomy'   => Changeloger_Post_Types::TAXONOMY_PRODUCT,
			'hide_empty' => false,
			'orderby'    => 'term_id',
			'order'      => 'ASC',
		] );

		if ( is_wp_error( $terms ) ) {
			return rest_ensure_response( [] );
		}

		global $wpdb;

		// Optimization: Batch count query to avoid N+1 problem
		// We need to count posts with status 'publish' OR 'draft' for each product term
		$sql = $wpdb->prepare(
			"SELECT tr.term_taxonomy_id, COUNT(p.ID) as count
             FROM {$wpdb->term_relationships} tr
             INNER JOIN {$wpdb->posts} p ON p.ID = tr.object_id
             WHERE p.post_type = %s
             AND p.post_status IN ('publish', 'draft')
             GROUP BY tr.term_taxonomy_id",
			Changeloger_Post_Types::POST_TYPE
		);

		$counts = $wpdb->get_results( $sql, OBJECT_K );

		$products = [];
		foreach ( $terms as $term ) {
			$term_id = $term->term_id;
			// For simple taxonomies, term_id often equals term_taxonomy_id, but we should be careful.
			// However, standard WP get_terms returns objects where term_id is the ID.
			// In standard WP usage, we group by term_taxonomy_id (which is unique per term per tax).
			// We can map via term_taxonomy_id if available on $term object, or assume standard setup.

			// get_terms returns WP_Term objects which have term_taxonomy_id
			$tt_id = $term->term_taxonomy_id;

			$count = isset( $counts[ $tt_id ] ) ? (int) $counts[ $tt_id ]->count : 0;

			$products[] = [
				'id'    => $term->term_id,
				'name'  => $term->name,
				'slug'  => $term->slug,
				'count' => $count,
			];
		}

		return rest_ensure_response( $products );
	}

	/**
	 * Create product
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function create_product( WP_REST_Request $request ) {
		// Check product limit for free users
		$is_premium = function_exists( 'cha_is_premium' ) && cha_is_premium();

		if ( ! $is_premium ) {
			$existing_products = get_terms( [
				'taxonomy'   => Changeloger_Post_Types::TAXONOMY_PRODUCT,
				'hide_empty' => false,
				'fields'     => 'count',
			] );

			$product_count = is_wp_error( $existing_products ) ? 0 : (int) $existing_products;

			if ( $product_count >= 1 ) {
				return new WP_Error(
					'product_limit_reached',
					__( 'Free version is limited to 1 product. Upgrade to Pro for unlimited products.', 'changeloger' ),
					[
						'status'      => 403,
						'upgrade_url' => function_exists( 'cha_fs' ) ? cha_fs()->get_upgrade_url() : '',
						'limit'       => 1,
						'current'     => $product_count,
					]
				);
			}
		}

		$name = sanitize_text_field( $request->get_param( 'name' ) );
		$slug = sanitize_title( $request->get_param( 'slug' ) ?: $name );

		if ( empty( $name ) ) {
			return new WP_Error( 'missing_name', __( 'Product name is required.', 'changeloger' ), [ 'status' => 400 ] );
		}

		$result = wp_insert_term( $name, Changeloger_Post_Types::TAXONOMY_PRODUCT, [
			'slug' => $slug,
		] );

		if ( is_wp_error( $result ) ) {
			return new WP_Error( 'create_failed', $result->get_error_message(), [ 'status' => 400 ] );
		}

		$term = get_term( $result['term_id'], Changeloger_Post_Types::TAXONOMY_PRODUCT );

		return rest_ensure_response( [
			'id'    => $term->term_id,
			'name'  => $term->name,
			'slug'  => $term->slug,
			'count' => $term->count,
		] );
	}

	/**
	 * Update product
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_product( WP_REST_Request $request ) {
		$id   = absint( $request->get_param( 'id' ) );
		$name = sanitize_text_field( $request->get_param( 'name' ) );
		$slug = sanitize_title( $request->get_param( 'slug' ) );

		$term = get_term( $id, Changeloger_Post_Types::TAXONOMY_PRODUCT );
		if ( ! $term || is_wp_error( $term ) ) {
			return new WP_Error( 'not_found', __( 'Product not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		$args = [];
		if ( $name ) {
			$args['name'] = $name;
		}
		if ( $slug ) {
			$args['slug'] = $slug;
		}

		$result = wp_update_term( $id, Changeloger_Post_Types::TAXONOMY_PRODUCT, $args );

		if ( is_wp_error( $result ) ) {
			return new WP_Error( 'update_failed', $result->get_error_message(), [ 'status' => 400 ] );
		}

		$term = get_term( $id, Changeloger_Post_Types::TAXONOMY_PRODUCT );

		return rest_ensure_response( [
			'id'    => $term->term_id,
			'name'  => $term->name,
			'slug'  => $term->slug,
			'count' => $term->count,
		] );
	}

	/**
	 * Delete product and all associated posts
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_product( WP_REST_Request $request ) {
		$id = absint( $request->get_param( 'id' ) );

		$term = get_term( $id, Changeloger_Post_Types::TAXONOMY_PRODUCT );
		if ( ! $term || is_wp_error( $term ) ) {
			return new WP_Error( 'not_found', __( 'Product not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		// Get all post types that use this taxonomy
		$post_types = [
			Changeloger_Post_Types::POST_TYPE,
			Changeloger_Post_Types::POST_TYPE_FEEDBACK,
			Changeloger_Post_Types::POST_TYPE_ROADMAP,
		];

		$deleted_counts = [
			'releases' => 0,
			'feedback' => 0,
			'roadmap'  => 0,
			'pages'    => 0,
		];

		// Delete associated release-hub pages for this product
		// Optimization: Filter by keyword to avoid scanning all pages
		$pages = get_posts( [
			'post_type'      => 'page',
			'posts_per_page' => -1,
			'post_status'    => 'any',
			's'              => 'cha/release-hub',
			'fields'         => 'ids',
		] );

		foreach ( $pages as $page_id ) {
			$page = get_post( $page_id );
			// Check if page contains release-hub block with this product ID.
			if ( has_block( 'cha/release-hub', $page ) ) {
				// Parse blocks to check productId attribute
				$blocks = parse_blocks( $page->post_content );
				foreach ( $blocks as $block ) {
					if ( 'cha/release-hub' === $block['blockName'] ) {
						$block_product_id = isset( $block['attrs']['productId'] ) ? (int) $block['attrs']['productId'] : 0;
						if ( $block_product_id === $id ) {
							wp_delete_post( $page->ID, true );
							$deleted_counts['pages']++;
							break;
						}
					}
				}
			}
		}

		// Delete all associated posts for each post type
		foreach ( $post_types as $post_type ) {
			$posts = get_posts( [
				'post_type'      => $post_type,
				'posts_per_page' => -1,
				'post_status'    => 'any',
				'fields'         => 'ids',
				'tax_query'      => [
					[
						'taxonomy' => Changeloger_Post_Types::TAXONOMY_PRODUCT,
						'field'    => 'term_id',
						'terms'    => $id,
					],
				],
			] );

			foreach ( $posts as $post_id ) {
				wp_delete_post( $post_id, true );

				if ( Changeloger_Post_Types::POST_TYPE === $post_type ) {
					$deleted_counts['releases']++;
				} elseif ( Changeloger_Post_Types::POST_TYPE_FEEDBACK === $post_type ) {
					$deleted_counts['feedback']++;
				} else {
					$deleted_counts['roadmap']++;
				}
			}
		}

		$result = wp_delete_term( $id, Changeloger_Post_Types::TAXONOMY_PRODUCT );

		if ( is_wp_error( $result ) ) {
			return new WP_Error( 'delete_failed', $result->get_error_message(), [ 'status' => 400 ] );
		}

		return rest_ensure_response( [
			'deleted' => true,
			'counts'  => $deleted_counts,
		] );
	}

	// ========== Badge Methods ==========

	/**
	 * Get all badges
	 *
	 * @return WP_REST_Response
	 */
	public function get_badges(): WP_REST_Response {
		$badges = Changeloger_Post_Types::get_release_badges();
		return rest_ensure_response( $badges );
	}

	/**
	 * Create a new badge
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function create_badge( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$data  = $request->get_json_params();
		$name  = sanitize_text_field( $data['name'] ?? '' );
		$color = sanitize_hex_color( $data['color'] ?? '#6366F1' );

		if ( empty( $name ) ) {
			return new WP_Error( 'missing_name', __( 'Badge name is required.', 'changeloger' ), [ 'status' => 400 ] );
		}

		// Create the taxonomy term.
		$result = wp_insert_term( $name, Changeloger_Post_Types::TAXONOMY_RELEASE_BADGE );

		if ( is_wp_error( $result ) ) {
			return new WP_Error( 'create_failed', $result->get_error_message(), [ 'status' => 400 ] );
		}

		$term_id = $result['term_id'];

		// Save color as term meta.
		update_term_meta( $term_id, '_badge_color', $color );

		return rest_ensure_response( [
			'id'    => $term_id,
			'name'  => $name,
			'slug'  => get_term( $term_id )->slug,
			'color' => $color,
		] );
	}

	/**
	 * Update a badge
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_badge( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$id    = absint( $request->get_param( 'id' ) );
		$data  = $request->get_json_params();
		$name  = isset( $data['name'] ) ? sanitize_text_field( $data['name'] ) : null;
		$color = isset( $data['color'] ) ? sanitize_hex_color( $data['color'] ) : null;

		// Check if term exists.
		$term = get_term( $id, Changeloger_Post_Types::TAXONOMY_RELEASE_BADGE );
		if ( ! $term || is_wp_error( $term ) ) {
			return new WP_Error( 'not_found', __( 'Badge not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		// Update term name if provided.
		if ( $name ) {
			$result = wp_update_term( $id, Changeloger_Post_Types::TAXONOMY_RELEASE_BADGE, [ 'name' => $name ] );
			if ( is_wp_error( $result ) ) {
				return new WP_Error( 'update_failed', $result->get_error_message(), [ 'status' => 400 ] );
			}
		}

		// Update color if provided.
		if ( $color ) {
			update_term_meta( $id, '_badge_color', $color );
		}

		// Get updated term.
		$updated_term = get_term( $id, Changeloger_Post_Types::TAXONOMY_RELEASE_BADGE );

		return rest_ensure_response( [
			'id'    => $id,
			'name'  => $updated_term->name,
			'slug'  => $updated_term->slug,
			'color' => get_term_meta( $id, '_badge_color', true ) ?: '#6366F1',
		] );
	}

	/**
	 * Delete a badge
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_badge( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		$id = absint( $request->get_param( 'id' ) );

		// Check if term exists.
		$term = get_term( $id, Changeloger_Post_Types::TAXONOMY_RELEASE_BADGE );
		if ( ! $term || is_wp_error( $term ) ) {
			return new WP_Error( 'not_found', __( 'Badge not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		// Delete the term.
		$result = wp_delete_term( $id, Changeloger_Post_Types::TAXONOMY_RELEASE_BADGE );

		if ( is_wp_error( $result ) ) {
			return new WP_Error( 'delete_failed', $result->get_error_message(), [ 'status' => 400 ] );
		}

		return rest_ensure_response( [ 'deleted' => true ] );
	}

	// ========== Feedback API Callbacks ==========

	/**
	 * Get feedback list with caching, filtering, and pagination.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_feedback_list( WP_REST_Request $request ) {
		$cache_version = get_option( 'cha_roadmap_cache_version', '1.3' );

		$cache_params = [
			'orderby'     => $request->get_param( 'orderby' ),
			'meta_key'    => $request->get_param( 'meta_key' ),
			'date_filter' => $request->get_param( 'date_filter' ),
			'per_page'    => $request->get_param( 'per_page' ),
			'page'        => $request->get_param( 'page' ),
			'order'       => $request->get_param( 'order' ),
			'status'      => $request->get_param( 'status' ),
			'product'     => $request->get_param( 'product' ),
			'category'    => $request->get_param( 'category' ),
			'search'      => $request->get_param( 'search' ),
			'is_manager'  => current_user_can( 'manage_options' ),
		];

		$cache_key   = 'cha_feedback_' . $cache_version . '_' . md5( serialize( $cache_params ) );
		$cached_data = get_transient( $cache_key );

		if ( false !== $cached_data ) {
			$items       = $cached_data['items'];
			$total       = $cached_data['total'];
			$total_pages = $cached_data['total_pages'];

			if ( is_user_logged_in() && ! empty( $items ) ) {
				$item_ids = array_column( $items, 'id' );
				update_meta_cache( 'post', $item_ids );

				$user_id = get_current_user_id();
				foreach ( $items as &$item ) {
					$item['has_upvoted']   = Changeloger_Feedback_Meta::has_user_upvoted( $item['id'], $user_id );
					$item['has_downvoted'] = Changeloger_Feedback_Meta::has_user_downvoted( $item['id'], $user_id );
					$item['has_voted']     = $item['has_upvoted'];
				}
			}

			return rest_ensure_response( [
				'items'            => $items,
				'total'            => $total,
				'total_pages'      => $total_pages,
				'is_limit_reached' => $cached_data['is_limit_reached'] ?? false,
				'is_premium'       => $cached_data['is_premium'] ?? true,
			] );
		}

		$orderby     = $request->get_param( 'orderby' ) ?: 'date';
		$meta_key    = $request->get_param( 'meta_key' );
		$date_filter = $request->get_param( 'date_filter' );

		$settings         = get_option( 'changeloger_settings', [] );
		$default_per_page = ! empty( $settings['items_per_page'] ) ? absint( $settings['items_per_page'] ) : 20;

		$args = [
			'post_type'      => Changeloger_Post_Types::POST_TYPE_FEEDBACK,
			'posts_per_page' => $request->get_param( 'per_page' ) ?: $default_per_page,
			'paged'          => $request->get_param( 'page' ) ?: 1,
			'orderby'        => $orderby,
			'order'          => $request->get_param( 'order' ) ?: 'DESC',
			'meta_query'     => [
				'relation' => 'AND',
				[
					'relation' => 'OR',
					[
						'key'     => '_cha_is_manual_roadmap',
						'compare' => 'NOT EXISTS',
					],
					[
						'key'     => '_cha_is_manual_roadmap',
						'value'   => '1',
						'compare' => '!=',
					],
				],
			],
		];

		if ( ! empty( $meta_key ) && in_array( $orderby, [ 'meta_value_num', 'meta_value' ], true ) ) {
			$args['meta_key'] = sanitize_text_field( $meta_key );
		}

		if ( 'this_week' === $date_filter ) {
			$args['date_query'] = [
				[ 'after' => '1 week ago', 'inclusive' => true ],
			];
		} elseif ( 'this_month' === $date_filter ) {
			$args['date_query'] = [
				[ 'after' => '1 month ago', 'inclusive' => true ],
			];
		}

		$status = $request->get_param( 'status' );
		if ( ! empty( $status ) ) {
			$args['meta_query'][] = [
				'key'   => Changeloger_Feedback_Meta::META_STATUS,
				'value' => sanitize_text_field( $status ),
			];
		}

		$product = $request->get_param( 'product' );
		if ( ! empty( $product ) ) {
			$args['tax_query'][] = [
				'taxonomy' => Changeloger_Post_Types::TAXONOMY_PRODUCT,
				'field'    => 'term_id',
				'terms'    => absint( $product ),
			];
		}

		$category = $request->get_param( 'category' );
		if ( ! empty( $category ) ) {
			$args['tax_query'][] = [
				'taxonomy' => Changeloger_Post_Types::TAXONOMY_FEEDBACK_CATEGORY,
				'field'    => 'slug',
				'terms'    => sanitize_text_field( $category ),
			];
		}

		$search = $request->get_param( 'search' );
		if ( ! empty( $search ) ) {
			$args['s'] = sanitize_text_field( $search );
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			$args['meta_query'][] = [
				'relation' => 'OR',
				[
					'key'   => Changeloger_Feedback_Meta::META_VISIBILITY,
					'value' => 'public',
				],
				[
					'key'     => Changeloger_Feedback_Meta::META_VISIBILITY,
					'compare' => 'NOT EXISTS',
				],
			];
		}

		$query = new WP_Query( $args );
		$items = [];

		if ( ! empty( $query->posts ) ) {
			$author_ids = array_unique( wp_list_pluck( $query->posts, 'post_author' ) );
			if ( ! empty( $author_ids ) ) {
				if ( function_exists( 'prime_user_caches' ) ) {
					prime_user_caches( $author_ids );
				} else {
					get_users( [ 'include' => $author_ids ] );
				}
			}
		}

		foreach ( $query->posts as $post ) {
			$items[] = $this->format_feedback_item( $post );
		}

		// Enforce premium limit of 20 for free users based on UNFILTERED total for the product
		$is_premium       = cha_is_premium();
		$total_count      = (int) $query->found_posts;
		$total_pages      = (int) $query->max_num_pages;
		$is_limit_reached = false;

		if ( ! empty( $product ) ) {
			$product_id       = absint( $product );
			$unfiltered_args  = [
				'post_type'      => Changeloger_Post_Types::POST_TYPE_FEEDBACK,
				'posts_per_page' => 1,
				'fields'         => 'ids',
				'tax_query'      => [
					[
						'taxonomy' => Changeloger_Post_Types::TAXONOMY_PRODUCT,
						'field'    => 'term_id',
						'terms'    => $product_id,
					],
				],
			];
			$unfiltered_query = new WP_Query( $unfiltered_args );
			$actual_total     = $unfiltered_query->found_posts;

			if ( ! $is_premium && $actual_total >= 20 ) {
				$is_limit_reached = true;
			}
		}

		if ( ! $is_premium ) {
			if ( $total_count > 20 ) {
				$items            = array_slice( $items, 0, 20 );
				$total_count      = 20;
				$total_pages      = 1;
			} elseif ( $total_count === 20 ) {
				// No slicing needed, but ensure totals are accurate if somehow different
				$total_count = 20;
				$total_pages = 1;
			}
		}

		$cached_items = $items;
		foreach ( $cached_items as &$item ) {
			$item['has_upvoted']   = false;
			$item['has_downvoted'] = false;
			$item['has_voted']     = false;
		}

		set_transient( $cache_key, [
			'items'            => $cached_items,
			'total'            => $total_count,
			'total_pages'      => $total_pages,
			'is_limit_reached' => $is_limit_reached,
			'is_premium'       => $is_premium,
		], 12 * HOUR_IN_SECONDS );

		return rest_ensure_response( [
			'items'            => $items,
			'total'            => $total_count,
			'total_pages'      => $total_pages,
			'is_limit_reached' => $is_limit_reached,
			'is_premium'       => $is_premium,
		] );
	}

	/**
	 * Get single feedback item.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_feedback( WP_REST_Request $request ) {
		$post = get_post( $request->get_param( 'id' ) );

		if ( ! $post || Changeloger_Post_Types::POST_TYPE_FEEDBACK !== $post->post_type ) {
			return new WP_Error( 'not_found', __( 'Feedback not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		// Prime meta cache in a single query to avoid N individual queries in format_feedback_item().
		update_meta_cache( 'post', [ $post->ID ] );

		$visibility = Changeloger_Feedback_Meta::get_visibility( $post->ID );
		if ( 'private' === $visibility && ! current_user_can( 'manage_options' ) ) {
			return new WP_Error( 'not_found', __( 'Feedback not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		return rest_ensure_response( $this->format_feedback_item( $post ) );
	}

	/**
	 * Create a new feedback item.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function create_feedback( WP_REST_Request $request ) {
		$title      = sanitize_text_field( $request->get_param( 'title' ) );
		$content    = wp_kses_post( $request->get_param( 'content' ) );
		$product_id = absint( $request->get_param( 'product_id' ) );
		$category   = sanitize_text_field( $request->get_param( 'category' ) );
		$is_manual  = (bool) $request->get_param( 'is_manual_roadmap' );
		$status     = sanitize_text_field( $request->get_param( 'status' ) );

		if ( empty( $title ) ) {
			return new WP_Error( 'missing_title', __( 'Title is required.', 'changeloger' ), [ 'status' => 400 ] );
		}

		$post_id = wp_insert_post( [
			'post_type'    => Changeloger_Post_Types::POST_TYPE_FEEDBACK,
			'post_title'   => $title,
			'post_content' => $content,
			'post_status'  => 'publish',
			'post_author'  => get_current_user_id(),
		] );

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

		$valid_statuses = array_keys( Changeloger_Post_Types::get_feedback_statuses() );
		if ( empty( $status ) || ! in_array( $status, $valid_statuses, true ) ) {
			$status = 'in_review';
		}
		update_post_meta( $post_id, Changeloger_Feedback_Meta::META_STATUS, $status );
		update_post_meta( $post_id, Changeloger_Feedback_Meta::META_UPVOTES, 0 );
		update_post_meta( $post_id, Changeloger_Feedback_Meta::META_VISIBILITY, 'public' );

		if ( true === $is_manual ) {
			update_post_meta( $post_id, '_cha_is_manual_roadmap', '1' );
		}

		if ( ! empty( $product_id ) ) {
			wp_set_object_terms( $post_id, $product_id, Changeloger_Post_Types::TAXONOMY_PRODUCT );
		}
		if ( ! empty( $category ) ) {
			wp_set_object_terms( $post_id, $category, Changeloger_Post_Types::TAXONOMY_FEEDBACK_CATEGORY );
		}

		do_action( 'cha_feedback_created', $post_id, $product_id, $category );

		return rest_ensure_response( $this->format_feedback_item( get_post( $post_id ) ) );
	}

	/**
	 * Update an existing feedback item.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_feedback( WP_REST_Request $request ) {
		$post_id = $request->get_param( 'id' );
		$post    = get_post( $post_id );

		if ( ! $post || Changeloger_Post_Types::POST_TYPE_FEEDBACK !== $post->post_type ) {
			return new WP_Error( 'not_found', __( 'Feedback not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		$title   = $request->get_param( 'title' );
		$content = $request->get_param( 'content' );
		if ( ! empty( $title ) || null !== $content ) {
			$update_data = [ 'ID' => $post_id ];
			if ( $title ) {
				$update_data['post_title'] = sanitize_text_field( $title );
			}
			if ( null !== $content ) {
				$update_data['post_content'] = wp_kses_post( $content );
			}
			wp_update_post( $update_data );
		}

		$status = $request->get_param( 'status' );
		if ( ! empty( $status ) ) {
			$old_status = Changeloger_Feedback_Meta::get_status( $post_id );
			update_post_meta( $post_id, Changeloger_Feedback_Meta::META_STATUS, sanitize_text_field( $status ) );
			if ( $old_status !== $status ) {
				do_action( 'cha_feedback_status_changed', $post_id, $status, $old_status );
			}
		}

		$visibility = $request->get_param( 'visibility' );
		if ( ! empty( $visibility ) ) {
			update_post_meta( $post_id, Changeloger_Feedback_Meta::META_VISIBILITY, sanitize_text_field( $visibility ) );
		}

		$product_id = $request->get_param( 'product_id' );
		if ( null !== $product_id ) {
			if ( ! empty( $product_id ) ) {
				wp_set_object_terms( $post_id, absint( $product_id ), Changeloger_Post_Types::TAXONOMY_PRODUCT );
			} else {
				wp_set_object_terms( $post_id, [], Changeloger_Post_Types::TAXONOMY_PRODUCT );
			}
		}

		return rest_ensure_response( $this->format_feedback_item( get_post( $post_id ) ) );
	}

	/**
	 * Delete a feedback item.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_feedback( WP_REST_Request $request ) {
		$post_id = $request->get_param( 'id' );
		$post    = get_post( $post_id );

		if ( ! $post || Changeloger_Post_Types::POST_TYPE_FEEDBACK !== $post->post_type ) {
			return new WP_Error( 'not_found', __( 'Feedback not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		wp_delete_post( $post_id, true );

		return rest_ensure_response( [ 'deleted' => true ] );
	}

	/**
	 * Toggle feedback upvote for the current user.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function toggle_feedback_upvote( WP_REST_Request $request ) {
		$post_id = $request->get_param( 'id' );
		$post    = get_post( $post_id );

		if ( ! $post || Changeloger_Post_Types::POST_TYPE_FEEDBACK !== $post->post_type ) {
			return new WP_Error( 'not_found', __( 'Feedback not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		$user_id = get_current_user_id();
		$result  = Changeloger_Feedback_Meta::toggle_upvote( $post_id, $user_id );

		return rest_ensure_response( $result );
	}

	/**
	 * Toggle feedback downvote for the current user.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function toggle_feedback_downvote( WP_REST_Request $request ) {
		$post_id = $request->get_param( 'id' );
		$post    = get_post( $post_id );

		if ( ! $post || Changeloger_Post_Types::POST_TYPE_FEEDBACK !== $post->post_type ) {
			return new WP_Error( 'not_found', __( 'Feedback not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		$user_id = get_current_user_id();
		$result  = Changeloger_Feedback_Meta::toggle_downvote( $post_id, $user_id );

		return rest_ensure_response( $result );
	}

	/**
	 * Get comments for a feedback item.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_feedback_comments( WP_REST_Request $request ) {
		$post_id = $request->get_param( 'id' );
		$post    = get_post( $post_id );

		if ( ! $post || Changeloger_Post_Types::POST_TYPE_FEEDBACK !== $post->post_type ) {
			return new WP_Error( 'not_found', __( 'Feedback not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		$visibility = Changeloger_Feedback_Meta::get_visibility( $post->ID );
		if ( 'private' === $visibility && ! current_user_can( 'manage_options' ) ) {
			return new WP_Error( 'not_found', __( 'Feedback not found.', 'changeloger' ), [ 'status' => 404 ] );
		}

		$comments = get_comments( [
			'post_id' => $post_id,
			'status'  => 'approve',
			'orderby' => 'comment_date',
			'order'   => 'ASC',
		] );

		$items = [];
		foreach ( $comments as $comment ) {
			$items[] = [
				'id'         => $comment->comment_ID,
				'author'     => $comment->comment_author,
				'author_id'  => (int) $comment->user_id,
				'content'    => $comment->comment_content,
				'date'       => $comment->comment_date,
				'avatar_url' => get_avatar_url( $comment->comment_author_email, [ 'size' => 48 ] ),
			];
		}

		return rest_ensure_response( $items );
	}

	/**
	 * Add a comment to a feedback item.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function add_feedback_comment( WP_REST_Request $request ) {
		$post_id = $request->get_param( 'id' );
		$content = sanitize_textarea_field( $request->get_param( 'content' ) );

		if ( empty( $content ) ) {
			return new WP_Error( 'empty_comment', __( 'Comment cannot be empty.', 'changeloger' ), [ 'status' => 400 ] );
		}

		$user = wp_get_current_user();

		$comment_id = wp_insert_comment( [
			'comment_post_ID'      => $post_id,
			'comment_author'       => $user->display_name,
			'comment_author_email' => $user->user_email,
			'comment_content'      => $content,
			'user_id'              => $user->ID,
			'comment_approved'     => 1,
		] );

		if ( ! $comment_id ) {
			return new WP_Error( 'comment_failed', __( 'Failed to add comment.', 'changeloger' ), [ 'status' => 500 ] );
		}

		wp_update_comment_count_now( $post_id );

		// Invalidate feedback list cache so comment count is fresh.
		$cache_version = get_option( 'cha_roadmap_cache_version', '1' );
		update_option( 'cha_roadmap_cache_version', (string) ( (int) $cache_version + 1 ) );

		$comment      = get_comment( $comment_id );
		$updated_post = get_post( $post_id );

		return rest_ensure_response( [
			'id'            => $comment->comment_ID,
			'author'        => $comment->comment_author,
			'author_id'     => (int) $comment->user_id,
			'content'       => $comment->comment_content,
			'date'          => $comment->comment_date,
			'avatar_url'    => get_avatar_url( $comment->comment_author_email, [ 'size' => 48 ] ),
			'comment_count' => (int) $updated_post->comment_count,
		] );
	}

	/**
	 * Get feedback stats grouped by status.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function get_feedback_stats( WP_REST_Request $request ) {
		$product_id = $request->get_param( 'product' );

		$cache_version = get_option( 'cha_roadmap_cache_version', '1' );
		$cache_key     = 'cha_stats_' . $cache_version . '_' . md5( 'product_' . (string) $product_id );
		$cached_data   = get_transient( $cache_key );

		if ( false !== $cached_data ) {
			return rest_ensure_response( $cached_data );
		}

		$statuses  = Changeloger_Post_Types::get_feedback_statuses();
		$by_status = [];

		global $wpdb;

		$post_type = Changeloger_Post_Types::POST_TYPE_FEEDBACK;
		$meta_key  = Changeloger_Feedback_Meta::META_STATUS;

		$join_sql  = "INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id";
		$where_sql = $wpdb->prepare(
			"p.post_type = %s AND p.post_status = 'publish' AND pm.meta_key = %s",
			$post_type,
			$meta_key
		);

		if ( $product_id ) {
			$join_sql  .= " INNER JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id";
			$join_sql  .= " INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id";
			$where_sql .= $wpdb->prepare(
				" AND tt.taxonomy = %s AND tt.term_id = %d",
				Changeloger_Post_Types::TAXONOMY_PRODUCT,
				absint( $product_id )
			);
		}

		$sql = "SELECT pm.meta_value as status, COUNT(p.ID) as count
				FROM {$wpdb->posts} p
				{$join_sql}
				WHERE {$where_sql}
				GROUP BY pm.meta_value";

		$results = $wpdb->get_results( $sql );

		$counts = [];
		$total  = 0;

		if ( $results ) {
			foreach ( $results as $row ) {
				$count                  = (int) $row->count;
				$counts[ $row->status ] = $count;
				$total                 += $count;
			}
		}

		foreach ( array_keys( $statuses ) as $s ) {
			$by_status[ $s ] = $counts[ $s ] ?? 0;
		}

		$data = [
			'total'     => $total,
			'by_status' => $by_status,
		];

		set_transient( $cache_key, $data, 12 * HOUR_IN_SECONDS );

		return rest_ensure_response( $data );
	}

	/**
	 * Format a feedback post into an API response item.
	 *
	 * @param WP_Post $post Post object.
	 * @return array Formatted feedback data.
	 */
	private function format_feedback_item( $post ) {
		$product_terms  = wp_get_post_terms( $post->ID, Changeloger_Post_Types::TAXONOMY_PRODUCT );
		$category_terms = wp_get_post_terms( $post->ID, Changeloger_Post_Types::TAXONOMY_FEEDBACK_CATEGORY );

		$user_id       = get_current_user_id();
		$has_upvoted   = $user_id ? Changeloger_Feedback_Meta::has_user_upvoted( $post->ID, $user_id ) : false;
		$has_downvoted = $user_id ? Changeloger_Feedback_Meta::has_user_downvoted( $post->ID, $user_id ) : false;
		$is_manual     = '1' === get_post_meta( $post->ID, '_cha_is_manual_roadmap', true );

		$upvotes   = Changeloger_Feedback_Meta::get_upvotes( $post->ID );
		$downvotes = Changeloger_Feedback_Meta::get_downvotes( $post->ID );
		$net_votes = $upvotes - $downvotes;

		return [
			'id'            => $post->ID,
			'title'         => $post->post_title,
			'content'       => $post->post_content,
			'excerpt'       => wp_trim_words( $post->post_content, 20 ),
			'status'        => Changeloger_Feedback_Meta::get_status( $post->ID ),
			'visibility'    => Changeloger_Feedback_Meta::get_visibility( $post->ID ),
			'upvotes'       => $upvotes,
			'downvotes'     => $downvotes,
			'votes'         => $net_votes,
			'net_votes'     => $net_votes,
			'has_voted'     => $has_upvoted,
			'has_upvoted'   => $has_upvoted,
			'has_downvoted' => $has_downvoted,
			'is_manual'     => $is_manual,
			'product'       => ! empty( $product_terms ) ? [
				'id'   => $product_terms[0]->term_id,
				'name' => $product_terms[0]->name,
			] : null,
			'category'      => ! empty( $category_terms ) ? [
				'slug' => $category_terms[0]->slug,
				'name' => $category_terms[0]->name,
			] : null,
			'author'        => [
				'id'     => $post->post_author,
				'name'   => get_the_author_meta( 'display_name', $post->post_author ),
				'avatar' => get_avatar_url( $post->post_author, [ 'size' => 48 ] ),
			],
			'date'          => $post->post_date,
			'comment_count' => (int) $post->comment_count,
		];
	}


	/**
	 * Invalidate feedback list caches by bumping cache version.
	 *
	 * @return void
	 */
	public function invalidate_feedback_cache() {
		$cache_version = get_option( 'cha_roadmap_cache_version', '1' );
		update_option( 'cha_roadmap_cache_version', (string) ( (int) $cache_version + 1 ) );
	}

}

