<?php
/**
 * Donations List Table
 *
 * @package AmbassadorHub\Donations\Admin
 */

namespace AmbassadorHub\Donations\Admin;

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

use AmbassadorHub\CustomFields\CustomFieldsManager;
use AmbassadorHub\Donations\DonationsModule;
use AmbassadorHub\Donors\DonorsModule;
use AmbassadorHub\Fundraisers\FundraisersHelpers;
use AmbassadorHub\Fundraisers\FundraisersModule;
use AmbassadorHub\Fundraisers\Admin\Settings\SettingsManager;

if ( ! class_exists( 'WP_List_Table' ) ) {
	require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}

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

/**
 * List table for the Donations admin screen.
 */
final class Donations_List_Table extends \WP_List_Table {

	/** Nonce action/field for the search/filter form — see Menu.php's wp_nonce_field() call. */
	private const NONCE_ACTION = 'ambh_donations_filter';
	private const NONCE_FIELD  = 'ambh_donations_nonce';

	/**
	 * Donor ID filter (0 = all donors).
	 *
	 * @var int
	 */
	private int $filter_donor_id;

	/**
	 * Root fundraiser ID filter (0 = all).
	 *
	 * @var int
	 */
	private int $filter_root_id;

	/**
	 * Fundraiser ID filter (0 = all fundraisers).
	 *
	 * @var int
	 */
	private int $filter_fundraiser_id;

	/**
	 * Active status tab slug.
	 *
	 * @var string
	 */
	private string $current_tab;

	/**
	 * Base URL including persistent query args (page + optional donor_id).
	 *
	 * @var string
	 */
	private string $base_url;

	/**
	 * Current search term.
	 *
	 * @var string
	 */
	private string $search_term;

	/** @var int */
	private int $count_all = 0;

	/** @var int */
	private int $count_completed = 0;

	/** @var int */
	private int $count_pending = 0;

	/** @var int */
	private int $count_failed = 0;

	/**
	 * Constructor.
	 *
	 * @param int    $filter_donor_id Donor ID to filter by (0 = all).
	 * @param string $current_tab     Active status tab slug.
	 * @param string $base_url        Base URL for pagination and tab links.
	 */
	public function __construct( int $filter_donor_id, string $current_tab, string $base_url ) {
		parent::__construct(
			array(
				'singular' => 'donation',
				'plural'   => 'donations',
				'ajax'     => false,
			)
		);

		$this->filter_donor_id = $filter_donor_id;
		$this->current_tab     = $current_tab;
		$this->base_url        = $base_url;

		// Search/filters only narrow this already-capability-gated screen's
		// display (no state is written), so an invalid/missing nonce just
		// fails open to the unfiltered view rather than wp_die()-ing.
		$nonce_valid = isset( $_GET[ self::NONCE_FIELD ] )
			&& wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET[ self::NONCE_FIELD ] ) ), self::NONCE_ACTION );

		$this->filter_root_id       = ( $nonce_valid && isset( $_GET['filter_root'] ) ) ? absint( $_GET['filter_root'] ) : 0;
		$this->filter_fundraiser_id = ( $nonce_valid && isset( $_GET['filter_fundraiser'] ) ) ? absint( $_GET['filter_fundraiser'] ) : 0;
		$this->search_term          = ( $nonce_valid && isset( $_REQUEST['s'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : '';
	}

	/**
	 * Define the columns.
	 *
	 * @return array<string,string>
	 */
	public function get_columns(): array {
		return array(
			'cb'              => '<input type="checkbox">',
			'donation'        => __( 'Donation', 'ambassadorhub' ),
			'root_fundraiser' => __( 'Root Fundraiser', 'ambassadorhub' ),
			'amount'          => __( 'Amount', 'ambassadorhub' ),
			'status'          => __( 'Status', 'ambassadorhub' ),
			'date'            => __( 'Date', 'ambassadorhub' ),
		);
	}

	/**
	 * Define sortable columns.
	 *
	 * @return array<string,array>
	 */
	protected function get_sortable_columns(): array {
		return array(
			'donation' => array( 'id', false ),
			'amount'   => array( 'amount', false ),
			'status'   => array( 'status', false ),
			'date'     => array( 'created_at', true ),
		);
	}

	/**
	 * Primary column name (determines where row actions are rendered).
	 *
	 * @return string
	 */
	protected function get_primary_column_name(): string {
		return 'donation';
	}

	/**
	 * Bulk actions available for the table.
	 *
	 * @return array<string,string>
	 */
	protected function get_bulk_actions(): array {
		return array();
	}

	/**
	 * Render root-fundraiser / fundraiser / donor dropdowns in the table nav bar.
	 *
	 * @param string $which 'top' or 'bottom'.
	 * @return void
	 */
	protected function extra_tablenav( $which ): void {
		if ( 'top' !== $which ) {
			return;
		}

		// Active donor filter chip.
		if ( $this->filter_donor_id ) {
			$donor      = DonorsModule::get( $this->filter_donor_id );
			$donor_name = $donor ? $donor->name : '#' . $this->filter_donor_id;
			$clear_url  = remove_query_arg( 'donor_id', $this->base_url );
			echo '<div class="ambh-filter-chip">';
			echo '<span class="ambh-filter-chip__label">'
				. esc_html__( 'Donor:', 'ambassadorhub' )
				. ' <strong>' . esc_html( $donor_name ) . '</strong></span>';
			echo ' <a href="' . esc_url( $clear_url ) . '" class="ambh-filter-chip__clear" title="'
				. esc_attr__( 'Clear donor filter', 'ambassadorhub' ) . '">&#10005;</a>';
			echo '</div>';
		}

		// Root fundraiser dropdown.
		$root_ids = get_posts(
			array(
				'post_type'      => FundraisersModule::POST_TYPE,
				'post_parent'    => 0,
				'posts_per_page' => -1,
				'orderby'        => 'title',
				'order'          => 'ASC',
				'post_status'    => 'any',
			)
		);

		// Fundraiser dropdown — scoped to selected root if active.
		$fundraiser_query_args = array(
			'post_type'      => FundraisersModule::POST_TYPE,
			'posts_per_page' => -1,
			'orderby'        => 'title',
			'order'          => 'ASC',
			'post_status'    => 'any',
		);
		if ( $this->filter_root_id ) {
			$descendant_ids                    = self::get_all_descendant_ids( $this->filter_root_id );
			$descendant_ids[]                  = $this->filter_root_id;
			$fundraiser_query_args['post__in'] = $descendant_ids;
		}
		$fundraisers = get_posts( $fundraiser_query_args );

		/**
		 * Build a parent→children map from a set of WP_Post objects.
		 *
		 * @param  \WP_Post[] $posts
		 * @return array<int, \WP_Post[]>
		 */
		$build_children_map = static function ( array $posts ): array {
			$pool_ids = array_flip( array_column( $posts, 'ID' ) );
			$map      = array();
			foreach ( $posts as $post ) {
				$parent           = ( $post->post_parent && isset( $pool_ids[ $post->post_parent ] ) )
					? $post->post_parent
					: 0;
				$map[ $parent ][] = $post;
			}
			return $map;
		};

		/**
		 * Recursively render <option> elements with depth indentation.
		 *
		 * @param int                   $parent_id
		 * @param int                   $depth
		 * @param array<int, \WP_Post[]> $children_map
		 */
		$render_fundraiser_options = null;
		$render_fundraiser_options = function ( int $parent_id, int $depth, array $children_map ) use ( &$render_fundraiser_options ): void {
			if ( empty( $children_map[ $parent_id ] ) ) {
				return;
			}
			foreach ( $children_map[ $parent_id ] as $fundraiser ) {
				$prefix = $depth > 0 ? str_repeat( "\u{00A0}\u{00A0}", $depth ) . '↳ ' : '';
				printf(
					'<option value="%d"%s>%s</option>',
					absint( $fundraiser->ID ),
					selected( $this->filter_fundraiser_id, $fundraiser->ID, false ),
					esc_html( $prefix . $fundraiser->post_title )
				);
				$render_fundraiser_options( $fundraiser->ID, $depth + 1, $children_map );
			}
		};

		echo '<div class="alignleft actions">';

		// Root fundraiser filter.
		echo '<select name="filter_root" id="filter-by-root">';
		echo '<option value="0">' . esc_html__( '— All Root Fundraisers —', 'ambassadorhub' ) . '</option>';
		foreach ( $root_ids as $root ) {
			printf(
				'<option value="%d"%s>%s</option>',
				absint( $root->ID ),
				selected( $this->filter_root_id, $root->ID, false ),
				esc_html( $root->post_title )
			);
		}
		echo '</select>';

		// Fundraiser filter.
		echo '<select name="filter_fundraiser" id="filter-by-fundraiser">';
		echo '<option value="0">' . esc_html__( '— All Fundraisers —', 'ambassadorhub' ) . '</option>';

		if ( $this->filter_root_id ) {
			// Root already selected — depth-indented list within that root.
			$children_map = $build_children_map( $fundraisers );
			$render_fundraiser_options( 0, 0, $children_map );
		} else {
			// No root selected — group by root using <optgroup>.
			foreach ( $root_ids as $root ) {
				$root_children = array_filter( $fundraisers, fn( $f ) => FundraisersHelpers::get_root_fundraiser_id( (int) $f->ID ) === (int) $root->ID );
				if ( empty( $root_children ) ) {
					continue;
				}
				echo '<optgroup label="' . esc_attr( $root->post_title ) . '">';
				$children_map = $build_children_map( array_values( $root_children ) );
				$render_fundraiser_options( 0, 0, $children_map );
				echo '</optgroup>';
			}
		}

		echo '</select>';

		submit_button( __( 'Filter', 'ambassadorhub' ), 'button', 'filter_action', false );
		echo '</div>';
	}

	/**
	 * Status-filter view links rendered above the table.
	 *
	 * @return array<string,string>
	 */
	protected function get_views(): array {
		$labels = array(
			'all'       => __( 'All', 'ambassadorhub' ),
			'completed' => __( 'Completed', 'ambassadorhub' ),
			'pending'   => __( 'Pending', 'ambassadorhub' ),
			'failed'    => __( 'Failed', 'ambassadorhub' ),
		);

		$counts = array(
			'all'       => $this->count_all,
			'completed' => $this->count_completed,
			'pending'   => $this->count_pending,
			'failed'    => $this->count_failed,
		);

		$views = array();
		foreach ( $labels as $slug => $label ) {
			$url   = 'all' === $slug
				? remove_query_arg( 'tab', $this->base_url )
				: add_query_arg( 'tab', $slug, $this->base_url );
			$class = $slug === $this->current_tab ? ' class="current"' : '';

			$views[ $slug ] = '<a href="' . esc_url( $url ) . '"' . $class . '>'
				. esc_html( $label )
				. ' <span class="count">(' . absint( $counts[ $slug ] ) . ')</span></a>';
		}

		return $views;
	}

	/**
	 * Handle bulk actions (currently none).
	 *
	 * @return void
	 */
	public function process_bulk_action(): void {
	}

	/**
	 * Fetch and paginate donations, and populate status-tab counts.
	 *
	 * @return void
	 */
	public function prepare_items(): void {
		global $wpdb;

		$table_name   = esc_sql( DonationsModule::get_table_name() );
		$donors_table = esc_sql( DonorsModule::get_table_name() );
		$posts_table  = esc_sql( $wpdb->posts );
		$per_page     = $this->get_items_per_page( 'ambh_donations_per_page', 20 );

		$allowed_orderby = array( 'id', 'amount', 'status', 'created_at' );
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$raw_orderby = isset( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : 'created_at';
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$raw_order = isset( $_GET['order'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_GET['order'] ) ) ) : 'DESC';

		$orderby = in_array( $raw_orderby, $allowed_orderby, true ) ? $raw_orderby : 'created_at';
		$order   = 'ASC' === $raw_order ? 'ASC' : 'DESC';

		$current_page = $this->get_pagenum();

		// Build WHERE clauses.
		$where_conditions = array( '1=1' );
		$where_values     = array();

		if ( $this->filter_donor_id ) {
			$where_conditions[] = 'd.donor_id = %d';
			$where_values[]     = $this->filter_donor_id;
		}

		if ( $this->filter_fundraiser_id ) {
			$where_conditions[] = 'd.fundraiser_id = %d';
			$where_values[]     = $this->filter_fundraiser_id;
		} elseif ( $this->filter_root_id ) {
			// Filter by all fundraisers that belong to this root (root + all descendants).
			$descendant_ids     = self::get_all_descendant_ids( $this->filter_root_id );
			$in_ids             = array_merge( array( $this->filter_root_id ), $descendant_ids );
			$in_placeholders    = implode( ', ', array_fill( 0, count( $in_ids ), '%d' ) );
			$where_conditions[] = "d.fundraiser_id IN ({$in_placeholders})";
			array_push( $where_values, ...$in_ids );
		}

		if ( 'all' !== $this->current_tab ) {
			$where_conditions[] = 'd.status = %s';
			$where_values[]     = $this->current_tab;
		}

		if ( $this->search_term ) {
			$like               = '%' . $wpdb->esc_like( $this->search_term ) . '%';
			$where_conditions[] = '(dn.name LIKE %s OR dn.email LIKE %s OR d.transaction_id LIKE %s OR CAST(d.id AS CHAR) LIKE %s)';
			array_push( $where_values, $like, $like, $like, $like );
		}

		$where_sql = implode( ' AND ', $where_conditions );

		$join_sql = "LEFT JOIN {$donors_table} dn ON dn.id = d.donor_id
		        LEFT JOIN {$posts_table} f ON f.ID = d.fundraiser_id";

		// Tab counts.
		$count_where_conditions = array( '1=1' );
		$count_where_values     = array();

		if ( $this->filter_donor_id ) {
			$count_where_conditions[] = 'd.donor_id = %d';
			$count_where_values[]     = $this->filter_donor_id;
		}
		if ( $this->filter_fundraiser_id ) {
			$count_where_conditions[] = 'd.fundraiser_id = %d';
			$count_where_values[]     = $this->filter_fundraiser_id;
		} elseif ( $this->filter_root_id ) {
			$descendant_ids           = self::get_all_descendant_ids( $this->filter_root_id );
			$in_ids                   = array_merge( array( $this->filter_root_id ), $descendant_ids );
			$in_placeholders          = implode( ', ', array_fill( 0, count( $in_ids ), '%d' ) );
			$count_where_conditions[] = "d.fundraiser_id IN ({$in_placeholders})";
			array_push( $count_where_values, ...$in_ids );
		}

		$count_where   = implode( ' AND ', $count_where_conditions );
		$count_agg_sql = "SELECT
			COUNT(*) AS count_all,
			SUM(CASE WHEN d.status = 'completed' THEN 1 ELSE 0 END) AS count_completed,
			SUM(CASE WHEN d.status = 'pending'   THEN 1 ELSE 0 END) AS count_pending,
			SUM(CASE WHEN d.status = 'failed'    THEN 1 ELSE 0 END) AS count_failed
			FROM {$table_name} d {$join_sql} WHERE {$count_where}";

		$counts_row = $count_where_values
			? $wpdb->get_row( $wpdb->prepare( $count_agg_sql, $count_where_values ) )
			: $wpdb->get_row( $count_agg_sql );

		$this->count_all       = (int) ( $counts_row->count_all ?? 0 );
		$this->count_completed = (int) ( $counts_row->count_completed ?? 0 );
		$this->count_pending   = (int) ( $counts_row->count_pending ?? 0 );
		$this->count_failed    = (int) ( $counts_row->count_failed ?? 0 );

		// Total items for pagination.
		$count_sql   = "SELECT COUNT(*) FROM {$table_name} d {$join_sql} WHERE {$where_sql}";
		$total_items = (int) ( $where_values
			? $wpdb->get_var( $wpdb->prepare( $count_sql, $where_values ) )
			: $wpdb->get_var( $count_sql ) );

		$offset = ( $current_page - 1 ) * $per_page;

		$select_sql = "SELECT d.*,
		               dn.name  AS linked_donor_name,
		               dn.email AS linked_donor_email,
		               f.post_title AS fundraiser_title
		        FROM {$table_name} d
		        {$join_sql}
		        WHERE {$where_sql}
		        ORDER BY d.{$orderby} {$order}
		        LIMIT %d OFFSET %d";

		$query_values = array_merge( $where_values, array( $per_page, $offset ) );
		$this->items  = $wpdb->get_results( $wpdb->prepare( $select_sql, $query_values ) );

		$this->set_pagination_args(
			array(
				'total_items' => $total_items,
				'per_page'    => $per_page,
				'total_pages' => (int) ceil( $total_items / $per_page ),
			)
		);

		$this->_column_headers = array(
			$this->get_columns(),
			$this->get_hidden_columns(),
			$this->get_sortable_columns(),
			$this->get_primary_column_name(),
		);
	}

	/**
	 * Return the currently hidden columns for this screen.
	 *
	 * @return string[]
	 */
	public function get_hidden_columns(): array {
		$screen = get_current_screen();
		if ( ! $screen ) {
			return array();
		}
		$hidden = get_user_meta( get_current_user_id(), 'manage' . $screen->id . 'columnshidden', true );
		return is_array( $hidden ) ? $hidden : array();
	}

	/**
	 * Checkbox column for bulk actions.
	 *
	 * @param object $item Donation row.
	 * @return string
	 */
	public function column_cb( $item ): string {
		return '<input type="checkbox" name="donation[]" value="' . absint( $item->id ) . '">';
	}

	/**
	 * Donation column (primary).
	 *
	 * @param object $item Donation row.
	 * @return string
	 */
	public function column_donation( $item ): string {
		$row_actions = array();

		if ( class_exists( CustomFieldsManager::class ) && ! empty( CustomFieldsManager::get_fields( 'donation' ) ) ) {
			$row_actions['custom_fields'] = '<a href="#" class="ambh-cf-toggle" data-id="' . absint( $item->id ) . '">' . esc_html__( 'Custom Fields', 'ambassadorhub' ) . '</a>';
		}

		$is_anonymous = ! empty( $item->is_anonymous );
		if ( $is_anonymous ) {
			$donor_label = __( 'Anonymous', 'ambassadorhub' );
		} else {
			$donor_label = DonationsModule::get_donor_name_for_display( $item );
		}

		$fundraiser_id    = ! empty( $item->fundraiser_id ) ? (int) $item->fundraiser_id : 0;
		$fundraiser_title = ! empty( $item->fundraiser_title ) ? $item->fundraiser_title : null;

		if ( $fundraiser_title && $fundraiser_id ) {
			$stats_url        = add_query_arg(
				array(
					'page'    => FundraisersModule::STATS_PAGE_SLUG,
					'post_id' => $fundraiser_id,
				),
				admin_url( 'admin.php' )
			);
			$fundraiser_label = '<a href="' . esc_url( $stats_url ) . '">' . esc_html( $fundraiser_title ) . '</a>';
		} elseif ( $fundraiser_title ) {
			$fundraiser_label = esc_html( $fundraiser_title );
		} else {
			$fundraiser_label = esc_html__( 'N/A', 'ambassadorhub' );
		}

		$label_html = sprintf(
			/* translators: 1: donation ID, 2: donor name, 3: fundraiser name */
			__( '#%1$d %2$s to %3$s', 'ambassadorhub' ),
			absint( $item->id ),
			esc_html( $donor_label ),
			$fundraiser_label
		);

		return '<strong class="row-title"><span>' . $label_html . '</span></strong>' . $this->row_actions( $row_actions );
	}

	/**
	 * Root fundraiser column — walks post_parent to find the root.
	 *
	 * @param object $item Donation row.
	 * @return string
	 */
	public function column_root_fundraiser( $item ): string {
		$fundraiser_id = (int) ( $item->fundraiser_id ?? 0 );
		if ( ! $fundraiser_id ) {
			return '—';
		}
		$root_id = FundraisersHelpers::get_root_fundraiser_id( $fundraiser_id );
		if ( ! $root_id ) {
			return '—';
		}
		$root = get_post( $root_id );
		if ( ! $root ) {
			return '—';
		}
		return '<a href="' . esc_url(
			add_query_arg(
				array(
					'page'        => 'ambassadorhub-donations',
					'filter_root' => $root_id,
				),
				admin_url( 'admin.php' )
			)
		) . '">' . esc_html( $root->post_title ) . '</a>';
	}

	/**
	 * Amount column with optional public-message tooltip.
	 *
	 * @param object $item Donation row.
	 * @return string
	 */
	public function column_amount( $item ): string {
		$out = esc_html( SettingsManager::format_currency( (float) $item->amount ) );

		if ( ! empty( $item->message ) ) {
			$out .= '<span class="ambh-donation-comment-icon" tabindex="0">'
				. '<span class="dashicons dashicons-format-chat" aria-hidden="true"></span>'
				. '<span class="ambh-donation-comment-tooltip">' . esc_html( $item->message ) . '</span>'
				. '</span>';
		}

		return $out;
	}

	/**
	 * Status column with coloured badge.
	 *
	 * @param object $item Donation row.
	 * @return string
	 */
	public function column_status( $item ): string {
		$color_map = array(
			'completed' => 'green',
			'pending'   => 'grey',
			'failed'    => 'red',
		);
		$color     = $color_map[ $item->status ] ?? 'grey';
		return '<span class="ambh-status-pill ambh-status-pill--' . esc_attr( $color ) . '">'
			. esc_html( ucfirst( $item->status ) )
			. '</span>';
	}

	/**
	 * Fallback column renderer.
	 *
	 * @param object $item        Donation row.
	 * @param string $column_name Column slug.
	 * @return string
	 */
	public function column_default( $item, $column_name ): string {
		if ( 'date' === $column_name ) {
			return esc_html(
				date_i18n(
					get_option( 'date_format' ) . ' ' . get_option( 'time_format' ),
					strtotime( $item->created_at )
				)
			);
		}
		return '';
	}

	/**
	 * Render a data row plus an optional hidden custom-fields details row.
	 *
	 * @param object $item Donation row.
	 *
	 * @return void
	 */
	public function single_row( $item ): void {
		parent::single_row( $item );

		if ( ! class_exists( CustomFieldsManager::class ) ) {
			return;
		}

		$fields = CustomFieldsManager::get_fields( 'donation' );
		if ( empty( $fields ) ) {
			return;
		}

		$donation_id = (int) $item->id;
		$meta        = CustomFieldsManager::get_all_donation_meta( $donation_id );
		$colspan     = count( $this->get_columns() );
		?>
		<tr class="ambh-cf-details-row" id="ambh-cf-row-<?php echo esc_attr( $donation_id ); ?>">
			<td colspan="<?php echo esc_attr( $colspan ); ?>" class="ambh-cf-details-cell">
				<div class="ambh-cf-details-inner">
					<table class="ambh-cf-details-table">
						<?php foreach ( $fields as $field ) : ?>
							<?php
							$meta_key = CustomFieldsManager::get_meta_key( $field['id'] );
							$raw      = $meta[ $meta_key ] ?? null;

							if ( 'checkbox' === ( $field['type'] ?? '' ) ) {
								$display = '1' === (string) $raw
									? '<span class="ambh-cf-check">&#10003; ' . esc_html__( 'Yes', 'ambassadorhub' ) . '</span>'
									: '<span class="ambh-cf-uncheck">&#8212;</span>';
							} elseif ( null !== $raw && '' !== $raw ) {
								$display = '<span>' . esc_html( (string) $raw ) . '</span>';
							} else {
								$display = '<span class="ambh-cf-empty">' . esc_html__( 'Not filled in', 'ambassadorhub' ) . '</span>';
							}
							?>
							<tr>
								<th><?php echo esc_html( $field['label'] ?? '' ); ?></th>
								<td><?php echo $display; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- each branch is individually escaped above. ?></td>
							</tr>
						<?php endforeach; ?>
					</table>
				</div>
			</td>
		</tr>
		<?php
	}

	/**
	 * Recursively collect all descendant fundraiser IDs under a given parent.
	 *
	 * @param int $parent_id Root/parent fundraiser post ID.
	 *
	 * @return int[]
	 */
	private static function get_all_descendant_ids( int $parent_id ): array {
		$ids      = array();
		$children = get_posts(
			array(
				'post_type'      => FundraisersModule::POST_TYPE,
				'post_parent'    => $parent_id,
				'posts_per_page' => -1,
				'fields'         => 'ids',
				'post_status'    => 'any',
			)
		);

		foreach ( $children as $child_id ) {
			$ids[] = (int) $child_id;
			$ids   = array_merge( $ids, self::get_all_descendant_ids( (int) $child_id ) );
		}

		return $ids;
	}
}

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