<?php
/**
 * Fundraisers REST API
 *
 * Provides REST API endpoints for fundraiser operations.
 *
 * @package AmbassadorHub\Fundraisers
 */

namespace AmbassadorHub\Fundraisers;

use AmbassadorHub\Admin\DateTimeHelper;
use AmbassadorHub\Donations\DonationsModule;
use AmbassadorHub\Fundraisers\FundraisersHelpers;
use AmbassadorHub\Spam\SpamShield;
use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;

use function apply_filters;
use function do_action;

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

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare

/**
 * Fundraisers API class.
 */
class FundraisersApi extends WP_REST_Controller {
	/**
	 * API namespace.
	 *
	 * @var string
	 */
	protected $namespace = \AmbassadorHub\Api\ApiModule::NAMESPACE;

	/**
	 * Resource base.
	 *
	 * @var string
	 */
	protected $rest_base = 'fundraisers';

	/**
	 * Initialize the API.
	 *
	 * @return void
	 */
	public static function init(): void {
		$instance = new self();
		add_action( 'ambh_register_rest_routes', array( $instance, 'register_routes' ) );
	}

	/**
	 * Constructor.
	 *
	 * @return void
	 */
	private function __construct() {
		// Private constructor for static init pattern.
	}

	/**
	 * Register REST API routes.
	 *
	 * @return void
	 */
	public function register_routes(): void {
		// GET /fundraisers - List fundraisers (block-editor use; admin-only).
		// POST /fundraisers - Create a new fundraiser.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base,
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_fundraisers' ),
					'permission_callback' => array( $this, 'list_fundraisers_permissions_check' ),
					'args'                => array(
						'root_id'  => array(
							'description'       => __( 'Filter by root fundraiser ID.', 'ambassadorhub' ),
							'type'              => 'integer',
							'default'           => 0,
							'sanitize_callback' => 'absint',
						),
						'per_page' => array(
							'description' => __( 'Maximum number of results to return.', 'ambassadorhub' ),
							'type'        => 'integer',
							'default'     => 100,
							'maximum'     => 100,
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'create_fundraiser' ),
					'permission_callback' => array( $this, 'create_fundraiser_permissions_check' ),
					'args'                => $this->get_create_fundraiser_params(),
				),
			)
		);

		// GET /fundraisers/{id} - Get fundraiser data.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)',
			array(
				array(
					'methods'             => WP_REST_Server::READABLE,
					'callback'            => array( $this, 'get_fundraiser' ),
					'permission_callback' => array( $this, 'get_fundraiser_permissions_check' ),
					'args'                => array(
						'id' => array(
							'description' => __( 'Fundraiser ID.', 'ambassadorhub' ),
							'type'        => 'integer',
							'required'    => true,
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::EDITABLE,
					'callback'            => array( $this, 'update_fundraiser' ),
					'permission_callback' => array( $this, 'update_fundraiser_permissions_check' ),
					'args'                => $this->get_update_fundraiser_params(),
				),
			)
		);

		// POST /fundraisers/{id}/image - Upload fundraiser image.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/image',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'upload_image' ),
					'permission_callback' => array( $this, 'update_fundraiser_permissions_check' ),
					'args'                => array(
						'id' => array(
							'description' => __( 'Fundraiser ID.', 'ambassadorhub' ),
							'type'        => 'integer',
							'required'    => true,
						),
					),
				),
				array(
					'methods'             => WP_REST_Server::DELETABLE,
					'callback'            => array( $this, 'delete_image' ),
					'permission_callback' => array( $this, 'update_fundraiser_permissions_check' ),
					'args'                => array(
						'id' => array(
							'description' => __( 'Fundraiser ID.', 'ambassadorhub' ),
							'type'        => 'integer',
							'required'    => true,
						),
					),
				),
			)
		);

		// GET /fundraisers/{id}/donations - Get donations list.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/donations',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_donations' ),
				'permission_callback' => '__return_true', // Public read: donation lists are displayed on public-facing fundraiser pages.
				'args'                => array(
					'id'    => array(
						'description' => __( 'Fundraiser ID.', 'ambassadorhub' ),
						'type'        => 'integer',
						'required'    => true,
					),
					'limit' => array(
						'description' => __( 'Maximum number of donations to return.', 'ambassadorhub' ),
						'type'        => 'integer',
						'default'     => 10,
						'minimum'     => 1,
						'maximum'     => 100,
					),
				),
			)
		);

		// POST /fundraisers/{id}/donations - Submit a donation.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/donations',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'create_donation' ),
				'permission_callback' => '__return_true', // Public: unauthenticated donors submit donations; input is validated and sanitised in the callback.
				'args'                => $this->get_create_donation_params(),
			)
		);

		// POST /fundraisers/{id}/close - Close a fundraiser and all its descendants immediately.
		register_rest_route(
			$this->namespace,
			'/' . $this->rest_base . '/(?P<id>[\d]+)/close',
			array(
				array(
					'methods'             => WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'close_fundraiser' ),
					'permission_callback' => array( $this, 'close_fundraiser_permissions_check' ),
					'args'                => array(
						'id' => array(
							'description' => __( 'Fundraiser ID.', 'ambassadorhub' ),
							'type'        => 'integer',
							'required'    => true,
						),
					),
				),
			)
		);

		// GET /fundraiser-form-fields - Render add-on contributed fields for a given
		// form context. The create-fundraiser-form wizard calls this when the
		// campaign or type selection changes so the field set can be swapped without
		// reloading the page.
		register_rest_route(
			$this->namespace,
			'/fundraiser-form-fields',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_fundraiser_form_fields_fragment' ),
				'permission_callback' => array( $this, 'fundraiser_form_fields_fragment_permissions_check' ),
				'args'                => array(
					'mode'          => array(
						'description' => __( 'Form mode.', 'ambassadorhub' ),
						'type'        => 'string',
						'enum'        => array( 'create', 'edit' ),
						'default'     => 'create',
					),
					'fundraiser_id' => array(
						'description'       => __( 'Existing fundraiser ID (edit mode).', 'ambassadorhub' ),
						'type'              => 'integer',
						'default'           => 0,
						'sanitize_callback' => 'absint',
					),
					'root_id'       => array(
						'description'       => __( 'Selected root fundraiser ID (0 if not yet chosen).', 'ambassadorhub' ),
						'type'              => 'integer',
						'default'           => 0,
						'sanitize_callback' => 'absint',
					),
					'type'          => array(
						'description' => __( 'Selected fundraiser type.', 'ambassadorhub' ),
						'type'        => 'string',
						'enum'        => array( '', 'team', 'individual' ),
						'default'     => '',
					),
					'parent_id'     => array(
						'description'       => __( 'Parent fundraiser ID (for sub-team fundraisers).', 'ambassadorhub' ),
						'type'              => 'integer',
						'default'           => 0,
						'sanitize_callback' => 'absint',
					),
				),
			)
		);
	}

	/**
	 * List fundraisers.
	 *
	 * Used by the Create Fundraiser Form block editor to populate the parent-fundraiser
	 * dropdown. Restricted to admins because it is only called from the block editor.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response
	 */
	public function get_fundraisers( WP_REST_Request $request ): WP_REST_Response {
		$root_id  = (int) $request->get_param( 'root_id' );
		$per_page = min( (int) $request->get_param( 'per_page' ), 100 );

		$query_args = array(
			'post_type'      => FundraisersModule::POST_TYPE,
			'post_status'    => array( 'publish', 'pending_verification' ),
			'posts_per_page' => $per_page ? $per_page : 100,
			'orderby'        => 'title',
			'order'          => 'ASC',
			'fields'         => 'all',
		);

		if ( $root_id > 0 ) {
			$all_ids        = $this->get_all_fundraiser_ids( $root_id );
			$descendant_ids = array_values( array_filter( $all_ids, fn( $id ) => $id !== $root_id ) );

			if ( empty( $descendant_ids ) ) {
				return new WP_REST_Response(
					array(
						'items'           => array(),
						'nesting_enabled' => FundraisersHelpers::root_allows_teams( $root_id ),
					),
					200
				);
			}

			$query_args['post__in'] = $descendant_ids;
		}

		$posts = get_posts( $query_args );

		$items = array_map(
			function ( \WP_Post $post ): array {
				return array(
					'id'    => $post->ID,
					'title' => $post->post_title,
				);
			},
			$posts
		);

		$nesting_enabled = $root_id > 0
			? FundraisersHelpers::root_allows_teams( $root_id )
			: false;

		return new WP_REST_Response(
			array(
				'items'           => $items,
				'nesting_enabled' => $nesting_enabled,
			),
			200
		);
	}

	/**
	 * Permission check for listing fundraisers.
	 *
	 * @return bool
	 */
	public function list_fundraisers_permissions_check(): bool {
		return current_user_can( 'ambh_manage_hub' );
	}

	/**
	 * Create a new fundraiser.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function create_fundraiser( WP_REST_Request $request ) {
		$title           = sanitize_text_field( $request->get_param( 'title' ) );
		$content         = wp_kses_post( $request->get_param( 'content' ) );
		$target_amount   = floatval( $request->get_param( 'target_amount' ) );
		$parent_id       = $request->get_param( 'parent_id' ) ? (int) $request->get_param( 'parent_id' ) : 0;
		$current_user_id = get_current_user_id();

		// Verified users get a published fundraiser; unverified users get a
		// 'pending_verification' fundraiser that auto-publishes on email verification.
		$post_status = \AmbassadorHub\Accounts\EmailVerification::user_is_verified( $current_user_id )
			? 'publish'
			: 'pending_verification';

		// Create the fundraiser post.
		// post_name is set explicitly so drafts get a real slug immediately;
		// without this, get_permalink() returns /?p=ID for drafts.
		$post_data = array(
			'post_title'   => $title,
			'post_name'    => sanitize_title( $title ),
			'post_content' => $content,
			'post_type'    => FundraisersModule::POST_TYPE,
			'post_status'  => $post_status,
			'post_parent'  => $parent_id,
			'post_author'  => $current_user_id,
		);

		$fundraiser_id = wp_insert_post( $post_data, true );

		if ( is_wp_error( $fundraiser_id ) ) {
			return new WP_Error(
				'creation_failed',
				__( 'Error creating fundraiser.', 'ambassadorhub' ),
				array( 'status' => 500 )
			);
		}

		// Set meta data.
		update_post_meta( $fundraiser_id, '_ambh_target_amount', $target_amount );

		// Save team flags.
		$is_group          = $request->get_param( 'is_group' ) === true || $request->get_param( 'is_group' ) === 'true';
		$allow_nested_team = $request->get_param( 'allow_nested_team' ) === true || $request->get_param( 'allow_nested_team' ) === 'true';
		update_post_meta( $fundraiser_id, FundraisersModule::META_IS_TEAM, $is_group ? '1' : '0' );
		if ( $is_group ) {
			update_post_meta( $fundraiser_id, FundraisersModule::META_ALLOW_NESTED, $allow_nested_team ? '1' : '0' );
		}

		// For root fundraisers, save whether teams can be created within them.
		if ( 0 === $parent_id ) {
			$teams_enabled = $request->get_param( 'teams_enabled' ) === true || $request->get_param( 'teams_enabled' ) === 'true';
			update_post_meta( $fundraiser_id, FundraisersModule::META_TEAMS_ENABLED, $teams_enabled ? '1' : '0' );
		}

		// Save end date if provided, clamped to the root fundraiser's end date.
		$end_date_input = $request->get_param( 'end_date' );
		if ( $end_date_input ) {
			$end_date_gmt = DateTimeHelper::convert_datetime_to_gmt( sanitize_text_field( $end_date_input ) );
			$root_id      = FundraisersHelpers::get_root_fundraiser_id( $fundraiser_id );
			$root_end_gmt = $root_id > 0 ? (string) get_post_meta( $root_id, '_ambh_end_date', true ) : '';
			if ( $root_end_gmt && $end_date_gmt > $root_end_gmt ) {
				$end_date_gmt = $root_end_gmt;
			}
			update_post_meta( $fundraiser_id, FundraisersModule::META_END_DATE, $end_date_gmt );
		}

		do_action( 'ambh_fundraiser_rest_saved', $fundraiser_id, $request, 'create' );

		$is_pending = 'pending_verification' === $post_status;
		$message    = $is_pending
			? __( 'Fundraiser created! Please verify your email address to publish it.', 'ambassadorhub' )
			: __( 'Fundraiser created successfully!', 'ambassadorhub' );

		// For pending_verification fundraisers, get_permalink() returns /?p=ID because
		// WordPress does not assign a pretty permalink until the post is published.
		// Build the URL from the slug stored on the post and the post type's rewrite slug.
		if ( $is_pending ) {
			$saved_post    = get_post( $fundraiser_id );
			$post_type_obj = get_post_type_object( FundraisersModule::POST_TYPE );
			$rewrite_slug  = ( $post_type_obj && is_array( $post_type_obj->rewrite ) )
				? ( $post_type_obj->rewrite['slug'] ?? 'fundraiser' )
				: 'fundraiser';
			$url           = home_url( trailingslashit( $rewrite_slug . '/' . $saved_post->post_name ) );
		} else {
			$url = get_permalink( $fundraiser_id );
		}

		$root_id_for_response   = 0 === $parent_id ? $fundraiser_id : FundraisersHelpers::get_root_fundraiser_id( $fundraiser_id );
		$teams_enabled_response = FundraisersHelpers::root_allows_teams( $root_id_for_response );

		return new WP_REST_Response(
			array(
				'id'            => $fundraiser_id,
				'title'         => $title,
				'status'        => $post_status,
				'message'       => $message,
				'url'           => $url,
				'teams_enabled' => $teams_enabled_response,
			),
			201
		);
	}

	/**
	 * Get fundraiser data.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_fundraiser( WP_REST_Request $request ) {
		$fundraiser_id = (int) $request->get_param( 'id' );
		$fundraiser    = get_post( $fundraiser_id );

		if ( ! $fundraiser || FundraisersModule::POST_TYPE !== $fundraiser->post_type ) {
			return new WP_Error(
				'fundraiser_not_found',
				__( 'Fundraiser not found.', 'ambassadorhub' ),
				array( 'status' => 404 )
			);
		}

		$target_amount = get_post_meta( $fundraiser_id, '_ambh_target_amount', true );
		$thumbnail_id  = get_post_thumbnail_id( $fundraiser_id );
		$thumbnail_url = $thumbnail_id ? wp_get_attachment_image_url( $thumbnail_id, 'medium' ) : '';

		$data = array(
			'id'            => $fundraiser_id,
			'title'         => $fundraiser->post_title,
			'content'       => $fundraiser->post_content,
			'target_amount' => $target_amount ? floatval( $target_amount ) : 0,
			'thumbnail_id'  => $thumbnail_id ? (int) $thumbnail_id : null,
			'thumbnail_url' => $thumbnail_url,
			'owner_id'      => (int) $fundraiser->post_author,
			'status'        => $fundraiser->post_status,
		);

		return new WP_REST_Response( $data, 200 );
	}

	/**
	 * Update fundraiser.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function update_fundraiser( WP_REST_Request $request ) {
		$fundraiser_id = (int) $request->get_param( 'id' );
		$fundraiser    = get_post( $fundraiser_id );

		if ( ! $fundraiser || FundraisersModule::POST_TYPE !== $fundraiser->post_type ) {
			return new WP_Error(
				'fundraiser_not_found',
				__( 'Fundraiser not found.', 'ambassadorhub' ),
				array( 'status' => 404 )
			);
		}

		// Update post data.
		$post_data = array(
			'ID' => $fundraiser_id,
		);

		if ( $request->has_param( 'title' ) ) {
			$post_data['post_title'] = sanitize_text_field( $request->get_param( 'title' ) );
		}

		if ( $request->has_param( 'content' ) ) {
			$post_data['post_content'] = wp_kses_post( $request->get_param( 'content' ) );
		}

		$result = wp_update_post( $post_data, true );

		if ( is_wp_error( $result ) ) {
			return new WP_Error(
				'update_failed',
				__( 'Error updating fundraiser.', 'ambassadorhub' ),
				array( 'status' => 500 )
			);
		}

		// Update meta data.
		if ( $request->has_param( 'target_amount' ) ) {
			update_post_meta( $fundraiser_id, '_ambh_target_amount', floatval( $request->get_param( 'target_amount' ) ) );
		}

		// Save end date if provided, clamped to the root fundraiser's end date.
		if ( $request->has_param( 'end_date' ) ) {
			$end_date_input = $request->get_param( 'end_date' );
			if ( $end_date_input ) {
				$end_date_gmt = DateTimeHelper::convert_datetime_to_gmt( sanitize_text_field( $end_date_input ) );
				$root_id      = FundraisersHelpers::get_root_fundraiser_id( $fundraiser_id );
				$root_end_gmt = $root_id > 0 ? (string) get_post_meta( $root_id, '_ambh_end_date', true ) : '';
				if ( $root_end_gmt && $end_date_gmt > $root_end_gmt ) {
					$end_date_gmt = $root_end_gmt;
				}
				update_post_meta( $fundraiser_id, FundraisersModule::META_END_DATE, $end_date_gmt );
			} else {
				delete_post_meta( $fundraiser_id, FundraisersModule::META_END_DATE );
			}
		}

		if ( $request->has_param( 'thumbnail_id' ) ) {
			$thumbnail_id = (int) $request->get_param( 'thumbnail_id' );
			if ( $thumbnail_id > 0 ) {
				set_post_thumbnail( $fundraiser_id, $thumbnail_id );
			}
		}

		do_action( 'ambh_fundraiser_rest_saved', $fundraiser_id, $request, 'update' );

		// Fire the updated action for published fundraisers so that automations
		// and transactional emails (ambh_fundraiser_updated hook) are triggered.
		$updated_post = get_post( $fundraiser_id );
		if ( $updated_post && 'publish' === $updated_post->post_status ) {
			/**
			 * Fires when a published fundraiser is updated via the REST API.
			 *
			 * @param int $fundraiser_id Fundraiser post ID.
			 */
			do_action( 'ambh_fundraiser_updated', $fundraiser_id );
		}

		return $this->get_fundraiser( $request );
	}

	/**
	 * Upload fundraiser image.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function upload_image( WP_REST_Request $request ) {
		$fundraiser_id = (int) $request->get_param( 'id' );
		$fundraiser    = get_post( $fundraiser_id );

		if ( ! $fundraiser || FundraisersModule::POST_TYPE !== $fundraiser->post_type ) {
			return new WP_Error(
				'fundraiser_not_found',
				__( 'Fundraiser not found.', 'ambassadorhub' ),
				array( 'status' => 404 )
			);
		}

		// Check if file was uploaded.
		if ( empty( $_FILES['file'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
			return new WP_Error(
				'no_file',
				__( 'No file uploaded.', 'ambassadorhub' ),
				array( 'status' => 400 )
			);
		}

		// Handle file upload.
		require_once ABSPATH . 'wp-admin/includes/image.php';
		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/media.php';

		$attachment_id = media_handle_upload( 'file', 0 );

		if ( is_wp_error( $attachment_id ) ) {
			return new WP_Error(
				'upload_failed',
				__( 'Error uploading: ', 'ambassadorhub' ) . $attachment_id->get_error_message(),
				array( 'status' => 500 )
			);
		}

		// Set as featured image.
		set_post_thumbnail( $fundraiser_id, $attachment_id );

		$thumbnail_url = wp_get_attachment_image_url( $attachment_id, 'medium' );

		return new WP_REST_Response(
			array(
				'attachment_id' => $attachment_id,
				'url'           => $thumbnail_url,
				'message'       => __( 'Image uploaded successfully.', 'ambassadorhub' ),
			),
			200
		);
	}

	/**
	 * Delete fundraiser image.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function delete_image( WP_REST_Request $request ) {
		$fundraiser_id = (int) $request->get_param( 'id' );
		$fundraiser    = get_post( $fundraiser_id );

		if ( ! $fundraiser || FundraisersModule::POST_TYPE !== $fundraiser->post_type ) {
			return new WP_Error(
				'fundraiser_not_found',
				__( 'Fundraiser not found.', 'ambassadorhub' ),
				array( 'status' => 404 )
			);
		}

		delete_post_thumbnail( $fundraiser_id );

		return new WP_REST_Response(
			array(
				'message' => __( 'Image deleted.', 'ambassadorhub' ),
			),
			200
		);
	}

	/**
	 * Get donations for a fundraiser.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function get_donations( WP_REST_Request $request ) {
		$fundraiser_id = (int) $request->get_param( 'id' );
		$limit         = (int) $request->get_param( 'limit' );
		$fundraiser    = get_post( $fundraiser_id );

		if ( ! $fundraiser || FundraisersModule::POST_TYPE !== $fundraiser->post_type ) {
			return new WP_Error(
				'fundraiser_not_found',
				__( 'Fundraiser not found.', 'ambassadorhub' ),
				array( 'status' => 404 )
			);
		}

		// Get all fundraiser IDs (including children).
		$fundraiser_ids = $this->get_all_fundraiser_ids( $fundraiser_id );

		// Get donations.
		global $wpdb;
		$table_name    = $wpdb->prefix . 'ambh_donations';
		$placeholders  = implode( ',', array_fill( 0, count( $fundraiser_ids ), '%d' ) );
		$donations_raw = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$table_name} 
				WHERE fundraiser_id IN ($placeholders) 
				AND status = 'completed' 
				ORDER BY created_at DESC 
				LIMIT %d",
				array_merge( $fundraiser_ids, array( $limit ) )
			)
		);

		$donations = array();
		foreach ( $donations_raw as $donation ) {
			$ambassador_name = null;

			// If donation is from a child fundraiser, get the ambassador name.
			if ( (int) $donation->fundraiser_id !== $fundraiser_id ) {
				$owner_id = (int) get_post_field( 'post_author', $donation->fundraiser_id );
				if ( $owner_id ) {
					$user            = get_user_by( 'id', $owner_id );
					$ambassador_name = $user ? $user->display_name : null;
				}
			}

			$donations[] = array(
				'id'              => (int) $donation->id,
				'fundraiser_id'   => (int) $donation->fundraiser_id,
				'donor_name'      => $donation->donor_name,
				'amount'          => floatval( $donation->amount ),
				'currency'        => $donation->currency,
				'message'         => $donation->message,
				'created_at'      => $donation->created_at,
				'ambassador_name' => $ambassador_name,
			);
		}

		return new WP_REST_Response( $donations, 200 );
	}

	/**
	 * Create a donation.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function create_donation( WP_REST_Request $request ) {
		$fundraiser_id = (int) $request->get_param( 'id' );
		$fundraiser    = get_post( $fundraiser_id );

		if ( ! $fundraiser || FundraisersModule::POST_TYPE !== $fundraiser->post_type ) {
			return new WP_Error(
				'fundraiser_not_found',
				__( 'Fundraiser not found.', 'ambassadorhub' ),
				array( 'status' => 404 )
			);
		}

		if ( ! FundraisersHelpers::fundraiser_is_open( $fundraiser_id ) ) {
			return new WP_Error(
				'fundraiser_closed',
				__( 'This fundraiser is no longer accepting donations.', 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		$donor_first_name = sanitize_text_field( $request->get_param( 'donor_first_name' ) );
		$donor_last_name  = sanitize_text_field( $request->get_param( 'donor_last_name' ) );
		$is_anonymous     = (int) $request->get_param( 'is_anonymous' );
		$donor_email      = sanitize_email( $request->get_param( 'donor_email' ) );
		$amount           = floatval( $request->get_param( 'amount' ) );
		$message          = sanitize_textarea_field( $request->get_param( 'message' ) );

		if ( ! SpamShield::throttle_by_ip( 'donation', SpamShield::donation_rate_limit(), 10 * MINUTE_IN_SECONDS )
			|| ! SpamShield::throttle_by_email( 'donation', $donor_email, 5, HOUR_IN_SECONDS ) ) {
			return new WP_Error(
				'rate_limited',
				__( 'Too many donation submissions. Please try again later.', 'ambassadorhub' ),
				array( 'status' => 429 )
			);
		}

		// Build display name: 'Anonymous' for anonymous donations; auto-composed otherwise.
		$donor_name = $is_anonymous
			? __( 'Anonymous', 'ambassadorhub' )
			: trim( $donor_first_name . ' ' . $donor_last_name );

		// Create donation record (pending payment).
		$donation_id = DonationsModule::create(
			array(
				'fundraiser_id'    => $fundraiser_id,
				'donor_first_name' => $donor_first_name,
				'donor_last_name'  => $donor_last_name,
				'donor_name'       => $donor_name,
				'donor_email'      => $donor_email,
				'is_anonymous'     => $is_anonymous,
				'amount'           => $amount,
				'currency'         => 'EUR',
				'status'           => 'pending',
				'message'          => $message,
				'notes'            => 'Created via REST API. Awaiting payment processing.',
			)
		);

		if ( ! $donation_id ) {
			return new WP_Error(
				'creation_failed',
				__( 'Error creating donation.', 'ambassadorhub' ),
				array( 'status' => 500 )
			);
		}

		/**
		 * Fires after a donation is created via this REST endpoint.
		 *
		 * Extensions (e.g. custom fields add-on) hook here to persist additional
		 * data tied to the new donation. Mirrors the action of the same name
		 * fired by DonationsApi::create_donation().
		 *
		 * @param int             $donation_id The new donation ID.
		 * @param WP_REST_Request $request     The REST request that created the donation.
		 */
		do_action( 'ambh_donation_rest_saved', $donation_id, $request );

		return new WP_REST_Response(
			array(
				'donation_id' => $donation_id,
				'message'     => __( 'Donation registered! Payment processing coming soon.', 'ambassadorhub' ),
			),
			201
		);
	}

	/**
	 * Check permissions for getting fundraiser data.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return bool|WP_Error
	 */
	public function get_fundraiser_permissions_check( WP_REST_Request $request ) {
		$fundraiser_id = (int) $request->get_param( 'id' );
		$owner_id      = (int) get_post_field( 'post_author', $fundraiser_id );

		// Only the owner can get full fundraiser data.
		if ( ! is_user_logged_in() ) {
			return new WP_Error(
				'not_logged_in',
				__( 'You must be logged in.', 'ambassadorhub' ),
				array( 'status' => 401 )
			);
		}

		if ( get_current_user_id() !== $owner_id ) {
			return new WP_Error(
				'forbidden',
				__( 'You are not authorized to view this fundraiser.', 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		return true;
	}

	/**
	 * Check permissions for updating fundraiser.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return bool|WP_Error
	 */
	public function update_fundraiser_permissions_check( WP_REST_Request $request ) {
		$fundraiser_id = (int) $request->get_param( 'id' );
		$owner_id      = (int) get_post_field( 'post_author', $fundraiser_id );

		// Only the owner can update the fundraiser.
		if ( ! is_user_logged_in() ) {
			return new WP_Error(
				'not_logged_in',
				__( 'You must be logged in.', 'ambassadorhub' ),
				array( 'status' => 401 )
			);
		}

		if ( get_current_user_id() !== $owner_id ) {
			return new WP_Error(
				'forbidden',
				__( 'You are not authorized to edit this fundraiser.', 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		return true;
	}

	/**
	 * Check permissions for creating fundraiser.
	 *
	 * For top-level fundraisers (parent_id = 0), only login is required.
	 * For nested fundraisers, nesting must be enabled on the parent's campaign
	 * and the requested depth must not exceed the campaign's configured max depth.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return bool|WP_Error
	 */
	public function create_fundraiser_permissions_check( WP_REST_Request $request ) {
		// User must be logged in.
		if ( ! is_user_logged_in() ) {
			return new WP_Error(
				'not_logged_in',
				__( 'You must be logged in.', 'ambassadorhub' ),
				array( 'status' => 401 )
			);
		}

		$parent_id = $request->get_param( 'parent_id' ) ? (int) $request->get_param( 'parent_id' ) : 0;
		$is_group  = $request->get_param( 'is_group' ) === true || $request->get_param( 'is_group' ) === 'true';

		// Top-level (root) fundraiser: requires the setting to be enabled or admin privileges.
		if ( 0 === $parent_id ) {
			if ( current_user_can( 'ambh_manage_hub' ) || FundraisersHelpers::ambassadors_can_create_root() ) {
				return true;
			}
			return new WP_Error(
				'root_creation_not_allowed',
				__( 'Creating root fundraisers is not enabled.', 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		// Nested fundraiser — parent must exist and be a fundraiser.
		$parent = get_post( $parent_id );
		if ( ! $parent || FundraisersModule::POST_TYPE !== $parent->post_type ) {
			return new WP_Error(
				'parent_not_found',
				__( 'Parent fundraiser not found.', 'ambassadorhub' ),
				array( 'status' => 404 )
			);
		}

		// Look up the root fundraiser for this hierarchy.
		$root_id = FundraisersHelpers::get_root_fundraiser_id( $parent_id );
		if ( ! $root_id ) {
			return new WP_Error(
				'root_not_found',
				__( 'Root fundraiser not found for this hierarchy.', 'ambassadorhub' ),
				array( 'status' => 404 )
			);
		}

		// Teams must be enabled on the root when creating a team.
		if ( $is_group && ! FundraisersHelpers::root_allows_teams( $root_id ) ) {
			return new WP_Error(
				'teams_not_allowed',
				__( 'Teams are not enabled for this fundraiser.', 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		// Calculate depth: root=1, so max nested depth is MAX_DEPTH.
		$parent_depth = FundraisersHelpers::calculate_fundraiser_depth( $parent_id );
		$new_depth    = $parent_depth + 1;

		if ( $new_depth > FundraisersHelpers::MAX_DEPTH ) {
			return new WP_Error(
				'max_depth_exceeded',
				__( 'Maximum nesting depth exceeded.', 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		// Teams can exist at depth 2 or 3; depth 4 must always be an individual fundraiser.
		if ( $is_group && $new_depth > ( FundraisersHelpers::MAX_DEPTH - 1 ) ) {
			return new WP_Error(
				'team_depth_exceeded',
				__( 'Teams cannot be nested this deep.', 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		// If creating a team inside an existing team (not the root), the parent must allow nested teams.
		if ( $is_group && $parent_id !== $root_id && ! FundraisersHelpers::fundraiser_allows_nested_team( $parent_id ) ) {
			return new WP_Error(
				'nested_teams_not_allowed',
				__( 'This team does not allow creating a team inside it.', 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		// A user can only have one personal fundraiser under a given parent.
		if ( ! $is_group && FundraisersHelpers::user_has_personal_fundraiser_under( $parent_id, get_current_user_id() ) ) {
			return new WP_Error(
				'personal_already_exists',
				__( 'You already have a personal fundraiser under this fundraiser.', 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		return true;
	}



	/**
	 * Close a fundraiser and all its descendants immediately.
	 *
	 * Sets the end date to the current time on every fundraiser in the hierarchy,
	 * transitions each to the 'ended' post status, and cancels any pending
	 * Action Scheduler jobs that would have closed them later.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response|WP_Error
	 */
	public function close_fundraiser( WP_REST_Request $request ) {
		$fundraiser_id = (int) $request->get_param( 'id' );
		$fundraiser    = get_post( $fundraiser_id );

		if ( ! $fundraiser || FundraisersModule::POST_TYPE !== $fundraiser->post_type ) {
			return new WP_Error(
				'fundraiser_not_found',
				__( 'Fundraiser not found.', 'ambassadorhub' ),
				array( 'status' => 404 )
			);
		}

		if ( 'ended' === $fundraiser->post_status ) {
			return new WP_Error(
				'already_ended',
				__( 'This fundraiser is already closed.', 'ambassadorhub' ),
				array( 'status' => 409 )
			);
		}

		$all_ids = $this->get_all_fundraiser_ids( $fundraiser_id );
		$now_gmt = gmdate( 'Y-m-d H:i:s' );

		foreach ( $all_ids as $id ) {
			update_post_meta( $id, FundraisersModule::META_END_DATE, $now_gmt );
			as_unschedule_all_actions( 'ambh_end_fundraiser', array( $id ), 'ambassadorhub' );
			FundraisersModule::end_fundraiser( $id );
		}

		return new WP_REST_Response(
			array( 'message' => __( 'Fundraiser closed successfully.', 'ambassadorhub' ) ),
			200
		);
	}

	/**
	 * Permission check for closing a fundraiser.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return bool|WP_Error
	 */
	public function close_fundraiser_permissions_check( WP_REST_Request $request ) {
		if ( ! is_user_logged_in() ) {
			return new WP_Error(
				'not_logged_in',
				__( 'You must be logged in.', 'ambassadorhub' ),
				array( 'status' => 401 )
			);
		}

		$fundraiser_id = (int) $request->get_param( 'id' );
		$owner_id      = (int) get_post_field( 'post_author', $fundraiser_id );

		if ( get_current_user_id() !== $owner_id && ! current_user_can( 'ambh_manage_hub' ) ) {
			return new WP_Error(
				'forbidden',
				__( 'You are not authorized to close this fundraiser.', 'ambassadorhub' ),
				array( 'status' => 403 )
			);
		}

		return true;
	}

	/**
	 * Get parameters for creating fundraiser.
	 *
	 * @return array
	 */
	protected function get_create_fundraiser_params(): array {
		$params = array(
			'title'             => array(
				'description' => __( 'Fundraiser title.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => true,
			),
			'content'           => array(
				'description' => __( 'Fundraiser description.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => false,
				'default'     => '',
			),
			'target_amount'     => array(
				'description' => __( 'Target amount (0 = no specific goal).', 'ambassadorhub' ),
				'type'        => 'number',
				'required'    => false,
				'minimum'     => 0,
				'default'     => 0,
			),
			'parent_id'         => array(
				'description' => __( 'Parent fundraiser ID (0 for top-level).', 'ambassadorhub' ),
				'type'        => 'integer',
				'required'    => false,
				'default'     => 0,
			),
			'is_group'          => array(
				'description' => __( 'Whether this is a group (true) or individual fundraiser (false).', 'ambassadorhub' ),
				'type'        => 'boolean',
				'required'    => false,
				'default'     => true,
			),
			'allow_nested_team' => array(
				'description' => __( 'Whether sub-teams can be created inside this team (only applies when is_group is true).', 'ambassadorhub' ),
				'type'        => 'boolean',
				'required'    => false,
				'default'     => false,
			),
			'teams_enabled'     => array(
				'description' => __( 'Whether teams can be created within this fundraiser (only applies to root fundraisers with parent_id=0).', 'ambassadorhub' ),
				'type'        => 'boolean',
				'required'    => false,
				'default'     => false,
			),
			'end_date'          => array(
				'description' => __( 'Fundraiser end date (Y-m-d\TH:i format, site timezone). Leave empty for no end date.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => false,
				'default'     => '',
			),
		);

		return apply_filters( 'ambh_fundraiser_create_params', $params );
	}

	/**
	 * Get parameters for updating fundraiser.
	 *
	 * @return array
	 */
	protected function get_update_fundraiser_params(): array {
		$params = array(
			'id'            => array(
				'description' => __( 'Fundraiser ID.', 'ambassadorhub' ),
				'type'        => 'integer',
				'required'    => true,
			),
			'title'         => array(
				'description' => __( 'Fundraiser title.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => false,
			),
			'content'       => array(
				'description' => __( 'Fundraiser description.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => false,
			),
			'target_amount' => array(
				'description' => __( 'Target amount.', 'ambassadorhub' ),
				'type'        => 'number',
				'required'    => false,
				'minimum'     => 0,
			),
			'thumbnail_id'  => array(
				'description' => __( 'Attachment ID of the image.', 'ambassadorhub' ),
				'type'        => 'integer',
				'required'    => false,
			),
			'end_date'      => array(
				'description' => __( 'Fundraiser end date (Y-m-d\TH:i format, site timezone). Pass an empty string to clear.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => false,
			),
		);

		return apply_filters( 'ambh_fundraiser_update_params', $params );
	}

	/**
	 * Get parameters for creating donation.
	 *
	 * @return array
	 */
	protected function get_create_donation_params(): array {
		return array(
			'id'               => array(
				'description' => __( 'Fundraiser ID.', 'ambassadorhub' ),
				'type'        => 'integer',
				'required'    => true,
			),
			'donor_first_name' => array(
				'description' => __( 'Donor first name.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => true,
			),
			'donor_last_name'  => array(
				'description' => __( 'Donor last name.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => true,
			),
			'donor_email'      => array(
				'description' => __( 'Donor email address.', 'ambassadorhub' ),
				'type'        => 'string',
				'format'      => 'email',
				'required'    => true,
			),
			'is_anonymous'     => array(
				'description' => __( 'Whether the donor wants to donate anonymously. Contact details are still stored.', 'ambassadorhub' ),
				'type'        => 'integer',
				'required'    => false,
				'default'     => 0,
				'enum'        => array( 0, 1 ),
			),
			'amount'           => array(
				'description' => __( 'Donation amount.', 'ambassadorhub' ),
				'type'        => 'number',
				'required'    => true,
				'minimum'     => 0.01,
			),
			'message'          => array(
				'description' => __( 'Public visible message.', 'ambassadorhub' ),
				'type'        => 'string',
				'required'    => false,
				'default'     => '',
			),
		);
	}

	/**
	 * Get all fundraiser IDs recursively.
	 *
	 * @param int $fundraiser_id Fundraiser ID.
	 *
	 * @return array
	 */
	protected function get_all_fundraiser_ids( int $fundraiser_id ): array {
		$ids = array( $fundraiser_id );

		// Get child fundraisers.
		$children = get_posts(
			array(
				'post_type'      => FundraisersModule::POST_TYPE,
				'post_parent'    => $fundraiser_id,
				'posts_per_page' => -1,
				'fields'         => 'ids',
			)
		);

		foreach ( $children as $child_id ) {
			$ids = array_merge( $ids, $this->get_all_fundraiser_ids( $child_id ) );
		}

		return $ids;
	}

	/**
	 * Permission check for the fundraiser-form-fields fragment endpoint.
	 *
	 * Logged-in users only — the create-fundraiser flow requires authentication,
	 * and we don't want anonymous traffic spinning up arbitrary fragment renders.
	 *
	 * In edit mode, also verifies the current user owns the referenced
	 * fundraiser (or holds edit_others_ambh_fundraisers): the fragment is
	 * rendered via the ambh_render_fundraiser_form_fields action, and add-ons
	 * hooked there may pre-fill it with that fundraiser's existing saved
	 * field values — without this check, any logged-in user could pass an
	 * arbitrary fundraiser_id and have another owner's data rendered back to them.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return bool|WP_Error
	 */
	public function fundraiser_form_fields_fragment_permissions_check( WP_REST_Request $request ) {
		if ( ! is_user_logged_in() ) {
			return new WP_Error(
				'rest_forbidden',
				__( 'You must be logged in to load the form fields.', 'ambassadorhub' ),
				array( 'status' => 401 )
			);
		}

		if ( 'edit' === $request->get_param( 'mode' ) ) {
			$fundraiser_id = (int) $request->get_param( 'fundraiser_id' );
			$fundraiser    = $fundraiser_id > 0 ? get_post( $fundraiser_id ) : null;

			if ( ! $fundraiser || FundraisersModule::POST_TYPE !== $fundraiser->post_type ) {
				return new WP_Error(
					'rest_forbidden',
					__( 'Fundraiser not found.', 'ambassadorhub' ),
					array( 'status' => 404 )
				);
			}

			$is_owner = get_current_user_id() === (int) $fundraiser->post_author;
			$is_admin = current_user_can( 'edit_others_ambh_fundraisers' );

			if ( ! $is_owner && ! $is_admin ) {
				return new WP_Error(
					'rest_forbidden',
					__( 'You do not have permission to edit this fundraiser.', 'ambassadorhub' ),
					array( 'status' => 403 )
				);
			}
		}

		return true;
	}

	/**
	 * Render the form-fields fragment for a given form context.
	 *
	 * Builds the $form_context array from query params, then fires the
	 * `ambh_render_fundraiser_form_fields` action (capturing the output) and
	 * collects the per-field JS config via the
	 * `ambh_fundraiser_form_fields_config` filter. Both hook points are the
	 * same ones used during the initial server-side render, so add-ons have
	 * one code path to maintain.
	 *
	 * @param WP_REST_Request $request Request object.
	 *
	 * @return WP_REST_Response
	 */
	public function get_fundraiser_form_fields_fragment( WP_REST_Request $request ): WP_REST_Response {
		$form_context = array(
			'fundraiser_id' => (int) $request->get_param( 'fundraiser_id' ),
			'mode'          => (string) $request->get_param( 'mode' ),
			'root_id'       => (int) $request->get_param( 'root_id' ),
			'type'          => (string) $request->get_param( 'type' ),
			'parent_id'     => (int) $request->get_param( 'parent_id' ),
		);

		ob_start();
		do_action( 'ambh_render_fundraiser_form_fields', $form_context );
		$html = (string) ob_get_clean();

		/**
		 * Filter the per-field client-side config (id, type, required) for the
		 * fields rendered above. Add-ons append their field entries so the form
		 * validator can re-register them after a fragment swap.
		 *
		 * @param array $config       Field config entries: [ [ 'id'=>..., 'type'=>..., 'required'=>... ], ... ].
		 * @param array $form_context The resolved form context array.
		 */
		$field_config = apply_filters( 'ambh_fundraiser_form_fields_config', array(), $form_context );

		return new WP_REST_Response(
			array(
				'html'        => $html,
				'fieldConfig' => array_values( (array) $field_config ),
			),
			200
		);
	}
}
