<?php
/**
 * Fundraiser Grid Block
 *
 * Displays a responsive card grid of fundraisers scoped to the current page
 * context. On a root fundraiser page the block shows direct children; on a
 * team fundraiser page it shows only that team's direct children (one level of
 * nesting). Preset mode allows the admin to pin a specific parent fundraiser in
 * the block editor.
 *
 * Each card shows: featured image (or placeholder), title, amount raised,
 * target amount, and a progress bar.
 *
 * @package AmbassadorHub\Fundraisers\Blocks\FundraiserGrid
 */

namespace AmbassadorHub\Fundraisers\Blocks\FundraiserGrid;

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

use AmbassadorHub\Fundraisers\Admin\Settings\SettingsManager;
use AmbassadorHub\Donations\DonationsModule;
use AmbassadorHub\Fundraisers\FundraisersModule;

/**
 * Block registration and server-side rendering.
 */
final class Block {

	/**
	 * Post meta key that stores the fundraiser target amount (EUR).
	 *
	 * No constant is defined on FundraisersModule for this key, so it is kept
	 * here as a private constant to avoid magic strings throughout this class.
	 */
	private const META_TARGET_AMOUNT = '_ambh_target_amount';

	// -------------------------------------------------------------------------
	// Boot
	// -------------------------------------------------------------------------

	/**
	 * Initialize the block.
	 *
	 * @return void
	 */
	public static function init(): void {
		add_action( 'init', array( self::class, 'register_block' ) );
	}

	// -------------------------------------------------------------------------
	// Registration
	// -------------------------------------------------------------------------

	/**
	 * Register block type, scripts, and styles.
	 *
	 * @return void
	 */
	public static function register_block(): void {
		$block_dir = AMBASSADOR_HUB_PATH . 'src/Fundraisers/Blocks/FundraiserGrid/';
		$block_url = AMBASSADOR_HUB_URL . 'src/Fundraisers/Blocks/FundraiserGrid/';

		// Editor script.
		$editor_handle = 'ambh-fundraiser-grid-editor';
		$editor_file   = $block_dir . 'index.js';
		if ( file_exists( $editor_file ) ) {
			wp_register_script(
				$editor_handle,
				$block_url . 'index.js',
				array( 'wp-blocks', 'wp-i18n', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-api-fetch' ),
				(string) filemtime( $editor_file ),
				true
			);
		}

		// Styles shared between editor and frontend.
		$style_file = $block_dir . 'style.css';
		wp_register_style(
			'ambh-fundraiser-grid',
			$block_url . 'style.css',
			array(),
			file_exists( $style_file ) ? (string) filemtime( $style_file ) : AMBASSADOR_HUB_VERSION
		);

		$block_args = array(
			'style'           => 'ambh-fundraiser-grid',
			'render_callback' => array( self::class, 'render_block' ),
		);

		if ( file_exists( $editor_file ) ) {
			$block_args['editor_script'] = $editor_handle;
		}

		register_block_type( $block_dir, $block_args );
	}

	// -------------------------------------------------------------------------
	// Rendering
	// -------------------------------------------------------------------------

	/**
	 * Server-side render callback.
	 *
	 * @param array $attributes Block attributes from block.json.
	 *
	 * @return string Rendered HTML.
	 */
	public static function render_block( array $attributes = array() ): string {

		// Return a lightweight editor placeholder — avoid heavy DB queries in the editor.
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return '<div class="ambh-fundraiser-grid ambh-block-preview">'
				. esc_html__( 'Fundraiser Grid — renders on the frontend.', 'ambassadorhub' )
				. '</div>';
		}

		$context = ContextResolver::resolve( $attributes );

		if ( ! $context['has_context'] ) {
			return '';
		}

		$view      = in_array( $attributes['view'] ?? '', array( 'grid', 'list' ), true ) ? $attributes['view'] : 'grid';
		$columns   = max( 1, min( 6, (int) ( $attributes['columns'] ?? 3 ) ) );
		$max_items = max( 0, (int) ( $attributes['maxItems'] ?? 0 ) );
		$order_by  = in_array( $attributes['orderBy'] ?? '', array( 'raised', 'title', 'date' ), true )
			? $attributes['orderBy']
			: 'raised';

		$fundraisers = self::query_fundraisers( $context, $order_by, $max_items );

		if ( empty( $fundraisers ) ) {
			return '';
		}

		$title      = trim( (string) ( $attributes['title'] ?? '' ) );
		$title_html = '' !== $title ? '<h2 class="ambh-block-title">' . esc_html( $title ) . '</h2>' : '';

		if ( 'list' === $view ) {
			return $title_html . self::render_list( $fundraisers );
		}

		$cards = '';
		foreach ( $fundraisers as $fundraiser ) {
			$cards .= self::render_card( $fundraiser );
		}

		return $title_html
			. '<div class="ambh-fundraiser-grid" style="--ambh-fg-columns:' . esc_attr( (string) $columns ) . ';">'
			. $cards
			. '</div>';
	}

	// -------------------------------------------------------------------------
	// Data
	// -------------------------------------------------------------------------

	/**
	 * Query and prepare enriched fundraiser data based on the resolved context.
	 *
	 * All matching fundraisers are fetched (posts_per_page = -1) so that
	 * PHP-side sorting (needed for "raised" order) and slicing (maxItems) are
	 * both consistent and correct.
	 *
	 * @param array  $context   Resolved context from ContextResolver.
	 * @param string $order_by  Sort field: 'raised', 'title', or 'date'.
	 * @param int    $max_items Maximum items to return (0 = all).
	 *
	 * @return array Array of fundraiser data maps.
	 */
	private static function query_fundraisers( array $context, string $order_by, int $max_items ): array {
		$parent_id = $context['parent_id'];

		$query_args = array(
			'post_type'      => FundraisersModule::POST_TYPE,
			'post_status'    => 'publish',
			'posts_per_page' => -1,
			'orderby'        => 'date',
			'order'          => 'DESC',
			'fields'         => 'ids',
			'post_parent'    => $parent_id,
		);

		// On team pages, only show sub-teams — individual members are shown by the Ambassadors block.
		if ( '1' === get_post_meta( $parent_id, FundraisersModule::META_IS_TEAM, true ) ) {
			$query_args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
				array(
					'key'     => FundraisersModule::META_IS_TEAM,
					'value'   => '1',
					'compare' => '=',
				),
			);
		}

		$query = new \WP_Query( $query_args );
		$ids   = array_map( 'intval', $query->posts ? $query->posts : array() );
		wp_reset_postdata();

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

		// Build enriched data array.
		$fundraisers = array();
		foreach ( $ids as $id ) {
			$post = get_post( $id );
			if ( ! $post ) {
				continue;
			}

			$total_raised  = self::get_total_raised_recursive( $id );
			$target_amount = (float) get_post_meta( $id, self::META_TARGET_AMOUNT, true );
			$thumbnail_url = get_the_post_thumbnail_url( $id, 'medium' );

			$fundraisers[] = array(
				'id'            => $id,
				'title'         => $post->post_title,
				'permalink'     => (string) get_permalink( $id ),
				'thumbnail_url' => (string) ( $thumbnail_url ? $thumbnail_url : '' ),
				'target_amount' => $target_amount,
				'total_raised'  => $total_raised,
			);
		}

		// Sort in PHP.
		switch ( $order_by ) {
			case 'raised':
				usort( $fundraisers, static fn( array $a, array $b ): int => $b['total_raised'] <=> $a['total_raised'] );
				break;
			case 'title':
				usort( $fundraisers, static fn( array $a, array $b ): int => strcmp( $a['title'], $b['title'] ) );
				break;
			case 'date':
			default:
				// WP_Query already returned posts ordered DESC by date.
				break;
		}

		// Apply item limit.
		if ( $max_items > 0 ) {
			$fundraisers = array_slice( $fundraisers, 0, $max_items );
		}

		return $fundraisers;
	}

	// -------------------------------------------------------------------------
	// Card rendering
	// -------------------------------------------------------------------------

	/**
	 * Render a single fundraiser card.
	 *
	 * @param array $fundraiser Fundraiser data map produced by query_fundraisers().
	 *
	 * @return string Card HTML.
	 */
	private static function render_card( array $fundraiser ): string {
		$target_amount = $fundraiser['target_amount'];
		$total_raised  = $fundraiser['total_raised'];
		$progress_pct  = $target_amount > 0.0
			? min( 100, round( ( $total_raised / $target_amount ) * 100, 1 ) )
			: 0;

		// Featured image or placeholder.
		$image_html = '' !== $fundraiser['thumbnail_url']
			? '<img src="' . esc_url( $fundraiser['thumbnail_url'] ) . '" alt="' . esc_attr( $fundraiser['title'] ) . '" loading="lazy">'
			: self::render_placeholder_image();

		// Amount raised.
		$raised_html = '<div class="ambh-fg-card__raised">'
			. esc_html(
				sprintf(
					/* translators: %s: formatted amount raised (e.g. €875.00) */
					__( '%s raised', 'ambassadorhub' ),
					SettingsManager::format_currency( $total_raised )
				)
			)
			. '</div>';

		// Target amount — only rendered when a target is set.
		$target_html = '';
		if ( $target_amount > 0.0 ) {
			$target_html = '<div class="ambh-fg-card__target">'
				. esc_html(
					sprintf(
						/* translators: %s: formatted target amount (e.g. €1,000.00) */
						__( 'of %s', 'ambassadorhub' ),
						SettingsManager::format_currency( $target_amount )
					)
				)
				. '</div>';
		}

		// Progress bar — only rendered when a target is set.
		$progress_html = '';
		if ( $target_amount > 0.0 ) {
			$progress_html = '<div class="ambh-fg-card__progress" role="progressbar"'
				. ' aria-valuenow="' . esc_attr( (string) $progress_pct ) . '"'
				. ' aria-valuemin="0" aria-valuemax="100">'
				. '<div class="ambh-fg-card__progress-fill" style="width:' . esc_attr( $progress_pct . '%' ) . ';"></div>'
				. '</div>';
		}

		return '<article class="ambh-fg-card">'
			. '<a href="' . esc_url( $fundraiser['permalink'] ) . '" class="ambh-fg-card__link">'
			. '<div class="ambh-fg-card__image">' . $image_html . '</div>'
			. '<div class="ambh-fg-card__body">'
			. '<h3 class="ambh-fg-card__title">' . esc_html( $fundraiser['title'] ) . '</h3>'
			. '<div class="ambh-fg-card__stats">' . $raised_html . $target_html . '</div>'
			. $progress_html
			. '</div>'
			. '</a>'
			. '</article>';
	}

	/**
	 * Render a placeholder image for fundraisers without a featured image.
	 *
	 * @return string HTML for the placeholder.
	 */
	private static function render_placeholder_image(): string {
		return '<div class="ambh-fg-card__placeholder" aria-hidden="true">'
			. '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">'
			. '<path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>'
			. '</svg>'
			. '</div>';
	}

	// -------------------------------------------------------------------------
	// List rendering
	// -------------------------------------------------------------------------

	/**
	 * Render fundraisers as a ranked ordered list (e.g. a top-10 leaderboard).
	 *
	 * @param array $fundraisers Fundraiser data maps produced by query_fundraisers().
	 *
	 * @return string Ordered list HTML.
	 */
	private static function render_list( array $fundraisers ): string {
		$items = '';
		foreach ( $fundraisers as $rank_index => $fundraiser ) {
			$items .= self::render_list_item( $fundraiser, $rank_index + 1 );
		}

		return '<ol class="ambh-fundraiser-list">' . $items . '</ol>';
	}

	/**
	 * Render a single row in the fundraiser list.
	 *
	 * Each row shows: rank number, avatar (thumbnail or initial letter),
	 * fundraiser name (linked), amount raised, and an optional thin progress bar.
	 *
	 * @param array $fundraiser Fundraiser data map.
	 * @param int   $rank       1-based rank position.
	 *
	 * @return string List item HTML.
	 */
	private static function render_list_item( array $fundraiser, int $rank ): string {
		$target_amount = $fundraiser['target_amount'];
		$total_raised  = $fundraiser['total_raised'];
		$progress_pct  = $target_amount > 0.0
			? min( 100, round( ( $total_raised / $target_amount ) * 100, 1 ) )
			: 0;

		// Avatar: thumbnail (circle-cropped) or fallback showing the first letter of the title.
		$avatar_inner = '' !== $fundraiser['thumbnail_url']
			? '<img src="' . esc_url( $fundraiser['thumbnail_url'] ) . '" alt="' . esc_attr( $fundraiser['title'] ) . '" loading="lazy">'
			: '<span class="ambh-fl-item__initial" aria-hidden="true">'
				. esc_html( mb_strtoupper( mb_substr( $fundraiser['title'], 0, 1 ) ) )
				. '</span>';

		// Progress bar — only rendered when a target amount is set.
		$progress_html = '';
		if ( $target_amount > 0.0 ) {
			$progress_html = '<div class="ambh-fl-item__progress" role="progressbar"'
				. ' aria-valuenow="' . esc_attr( (string) $progress_pct ) . '"'
				. ' aria-valuemin="0" aria-valuemax="100">'
				. '<div class="ambh-fl-item__progress-fill" style="width:' . esc_attr( $progress_pct . '%' ) . ';"></div>'
				. '</div>';
		}

		/* translators: %d: rank position number */
		$rank_label = sprintf( __( 'Rank %d', 'ambassadorhub' ), $rank );

		return '<li class="ambh-fl-item">'
			. '<a href="' . esc_url( $fundraiser['permalink'] ) . '" class="ambh-fl-item__link">'
			. '<span class="ambh-fl-item__rank" aria-label="' . esc_attr( $rank_label ) . '">' . esc_html( (string) $rank ) . '</span>'
			. '<span class="ambh-fl-item__avatar">' . $avatar_inner . '</span>'
			. '<span class="ambh-fl-item__name">' . esc_html( $fundraiser['title'] ) . '</span>'
			. '<span class="ambh-fl-item__raised">' . esc_html( SettingsManager::format_currency( $total_raised ) ) . '</span>'
			. '</a>'
			. $progress_html
			. '</li>';
	}

	/**
	 * Get total raised for a fundraiser including all child fundraisers recursively.
	 *
	 * @param int $fundraiser_id Fundraiser post ID.
	 *
	 * @return float
	 */
	private static function get_total_raised_recursive( int $fundraiser_id ): float {
		global $wpdb;
		$table_name   = esc_sql( DonationsModule::get_table_name() );
		$child_ids    = self::get_children_ids_recursive( $fundraiser_id );
		$all_ids      = array_merge( array( $fundraiser_id ), $child_ids );
		$placeholders = implode( ', ', array_fill( 0, count( $all_ids ), '%d' ) );

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $table_name from $wpdb->prefix; placeholders built dynamically from count; no caching for aggregate totals.
		$total = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COALESCE(SUM(amount), 0) FROM {$table_name}
				WHERE fundraiser_id IN ({$placeholders}) AND status = 'completed'",
				...$all_ids
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare

		return (float) $total;
	}

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

		$all = $children;
		foreach ( $children as $child_id ) {
			$all = array_merge( $all, self::get_children_ids_recursive( (int) $child_id ) );
		}

		return $all;
	}
}
