<?php
/**
 * Fundraisers Domain Helpers
 *
 * Pure, stateless utility functions for the Fundraisers domain.
 *
 * @package AmbassadorHub\Fundraisers
 */

namespace AmbassadorHub\Fundraisers;

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

/**
 * Stateless domain helpers for the Fundraisers module.
 *
 * All methods are static; this class is never instantiated.
 */
final class FundraisersHelpers {

	/** Maximum hierarchy depth: root → team → sub-team → individual. */
	public const MAX_DEPTH = 4;

	// -------------------------------------------------------------------------
	// Root-fundraiser resolution
	// -------------------------------------------------------------------------

	/**
	 * Returns the ID of the root (top-level) fundraiser in a hierarchy.
	 *
	 * Traverses the post_parent chain upward until a fundraiser with no parent
	 * is found. For a root fundraiser (post_parent=0) the ID itself is returned.
	 * Caps traversal at 10 to protect against bad data.
	 *
	 * @param int $fundraiser_id Fundraiser post ID.
	 *
	 * @return int Root fundraiser ID, or 0 if the post is invalid.
	 */
	public static function get_root_fundraiser_id( int $fundraiser_id ): int {
		if ( $fundraiser_id <= 0 ) {
			return 0;
		}

		$current_id = $fundraiser_id;

		for ( $i = 0; $i < 10; $i++ ) {
			$parent_id = (int) wp_get_post_parent_id( $current_id );

			if ( $parent_id <= 0 ) {
				return $current_id;
			}

			$current_id = $parent_id;
		}

		return $current_id;
	}

	// -------------------------------------------------------------------------
	// Root-level config helpers
	// -------------------------------------------------------------------------

	/**
	 * Returns true when the root fundraiser has teams enabled.
	 *
	 * @param int $root_id Root fundraiser post ID.
	 *
	 * @return bool
	 */
	public static function root_allows_teams( int $root_id ): bool {
		if ( $root_id <= 0 ) {
			return false;
		}

		return get_post_meta( $root_id, FundraisersModule::META_TEAMS_ENABLED, true ) === '1';
	}

	/**
	 * Returns true when ambassadors are allowed to create their own root (depth-1) fundraisers.
	 *
	 * Controlled by the plugin setting ambh_fundraisers_ambassadors_can_create_top_level.
	 *
	 * @return bool
	 */
	public static function ambassadors_can_create_root(): bool {
		return get_option( 'ambh_fundraisers_ambassadors_can_create_top_level', '0' ) === '1';
	}

	// -------------------------------------------------------------------------
	// Fundraiser status & identity checks
	// -------------------------------------------------------------------------

	/**
	 * Returns true when the fundraiser has an active status.
	 *
	 * Active means 'publish' or 'pending_verification'.
	 *
	 * @param int $fundraiser_id Fundraiser post ID.
	 *
	 * @return bool
	 */
	public static function fundraiser_is_active( int $fundraiser_id ): bool {
		if ( $fundraiser_id <= 0 ) {
			return false;
		}

		$status = get_post_status( $fundraiser_id );

		return in_array( $status, array( 'publish', 'pending_verification' ), true );
	}

	/**
	 * Returns the effective end date (GMT) for a fundraiser.
	 *
	 * The effective end date is the earlier of:
	 *   1. The root fundraiser's own end date (always respected).
	 *   2. The fundraiser's own end date (optional).
	 *
	 * For a root fundraiser the two sources collapse to one.
	 * Returns null when neither source has an end date.
	 *
	 * @param int $fundraiser_id Fundraiser post ID.
	 *
	 * @return string|null GMT datetime string 'Y-m-d H:i:s', or null.
	 */
	public static function get_effective_end_date_gmt( int $fundraiser_id ): ?string {
		$root_id = self::get_root_fundraiser_id( $fundraiser_id );

		// Root end date (from the root fundraiser's own _ambh_end_date).
		// For a root fundraiser this is the same post, so it's covered by $own_end.
		$root_end = ( $root_id > 0 && $root_id !== $fundraiser_id )
			? (string) get_post_meta( $root_id, '_ambh_end_date', true )
			: '';

		$own_end = (string) get_post_meta( $fundraiser_id, FundraisersModule::META_END_DATE, true );

		if ( ! $root_end && ! $own_end ) {
			return null;
		}
		if ( ! $root_end ) {
			return $own_end ? $own_end : null;
		}
		if ( ! $own_end ) {
			return $root_end ? $root_end : null;
		}

		return strtotime( $own_end ) <= strtotime( $root_end ) ? $own_end : $root_end;
	}

	/**
	 * Returns true when the fundraiser (or its root) is configured to accept direct donations.
	 *
	 * Only root fundraisers carry this setting. Non-root fundraisers always return true
	 * because the restriction is set at the root level and child fundraisers are unaffected.
	 * Defaults to true for backwards-compatibility with existing root fundraisers.
	 *
	 * @param int $fundraiser_id Fundraiser post ID.
	 *
	 * @return bool
	 */
	public static function fundraiser_accepts_donations( int $fundraiser_id ): bool {
		if ( $fundraiser_id <= 0 ) {
			return false;
		}

		// Only root fundraisers carry the flag; non-root fundraisers are unaffected.
		if ( (int) wp_get_post_parent_id( $fundraiser_id ) !== 0 ) {
			return true;
		}

		$value = get_post_meta( $fundraiser_id, FundraisersModule::META_ACCEPTS_DONATIONS, true );

		// Default true when the meta has never been set (existing fundraisers).
		return '' === $value || '1' === $value;
	}

	/**
	 * Returns true when the fundraiser is open for new donations.
	 *
	 * A fundraiser is open when:
	 *   1. Its root fundraiser is published.
	 *   2. The effective end date has not yet passed.
	 *
	 * For root fundraisers this reduces to: status=publish AND end date not passed.
	 *
	 * @param int $fundraiser_id Fundraiser post ID.
	 *
	 * @return bool
	 */
	public static function fundraiser_is_open( int $fundraiser_id ): bool {
		$root_id = self::get_root_fundraiser_id( $fundraiser_id );

		if ( 'publish' !== get_post_status( $root_id ) ) {
			return false;
		}

		$effective_end = self::get_effective_end_date_gmt( $fundraiser_id );
		if ( null !== $effective_end && strtotime( $effective_end ) <= time() ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns true when the fundraiser is a team fundraiser, regardless of status.
	 *
	 * @param int $fundraiser_id Fundraiser post ID.
	 *
	 * @return bool
	 */
	public static function fundraiser_is_team( int $fundraiser_id ): bool {
		if ( $fundraiser_id <= 0 ) {
			return false;
		}

		return get_post_meta( $fundraiser_id, FundraisersModule::META_IS_TEAM, true ) === '1';
	}

	/**
	 * Returns true when the fundraiser is an active team fundraiser.
	 *
	 * @param int $fundraiser_id Fundraiser post ID.
	 *
	 * @return bool
	 */
	public static function fundraiser_is_active_team( int $fundraiser_id ): bool {
		if ( $fundraiser_id <= 0 ) {
			return false;
		}

		if ( ! self::fundraiser_is_active( $fundraiser_id ) ) {
			return false;
		}

		return self::fundraiser_is_team( $fundraiser_id );
	}

	/**
	 * Returns true when a post is a valid parent fundraiser.
	 *
	 * @param int|\WP_Post|null $post Post ID or WP_Post object.
	 *
	 * @return bool
	 */
	public static function is_valid_parent_fundraiser( int|\WP_Post|null $post ): bool {
		$post = get_post( $post );

		if ( ! $post ) {
			return false;
		}

		return FundraisersModule::POST_TYPE === $post->post_type
			&& in_array( $post->post_status, array( 'publish', 'pending_verification' ), true );
	}

	// -------------------------------------------------------------------------
	// Team / nesting rules
	// -------------------------------------------------------------------------

	/**
	 * Returns true when the fundraiser allows a nested sub-team inside it.
	 *
	 * @param int $fundraiser_id Fundraiser post ID.
	 *
	 * @return bool
	 */
	public static function fundraiser_allows_nested_team( int $fundraiser_id ): bool {
		if ( $fundraiser_id <= 0 ) {
			return false;
		}

		if ( get_post_meta( $fundraiser_id, FundraisersModule::META_IS_TEAM, true ) !== '1' ) {
			return false;
		}

		return get_post_meta( $fundraiser_id, FundraisersModule::META_ALLOW_NESTED, true ) === '1';
	}

	/**
	 * Returns true when creating a new team fundraiser is allowed.
	 *
	 * Validates:
	 * - Root fundraiser has teams enabled
	 * - Parent fundraiser (if provided) is active, open, and allows nested teams
	 *
	 * @param int $root_id   Root fundraiser post ID (depth=1).
	 * @param int $parent_id Parent fundraiser ID (0 = creating a direct child of root).
	 *
	 * @return bool
	 */
	public static function team_creation_allowed( int $root_id, int $parent_id = 0 ): bool {
		if ( ! self::root_allows_teams( $root_id ) ) {
			return false;
		}

		if ( $parent_id > 0 && ! self::fundraiser_is_active( $parent_id ) ) {
			return false;
		}

		if ( $parent_id > 0 && ! self::fundraiser_is_open( $parent_id ) ) {
			return false;
		}

		if ( $parent_id > 0 && $parent_id !== $root_id && ! self::fundraiser_allows_nested_team( $parent_id ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns true when creating a new individual fundraiser is allowed under the given parent.
	 *
	 * Individuals can be created under any active, open fundraiser (root, team, or sub-fundraiser)
	 * as long as the resulting depth does not exceed MAX_DEPTH.
	 *
	 * @param int $parent_id Parent fundraiser ID (0 = no parent).
	 *
	 * @return bool
	 */
	public static function individual_creation_allowed( int $parent_id = 0 ): bool {
		if ( $parent_id <= 0 ) {
			return false;
		}

		if ( ! self::fundraiser_is_active( $parent_id ) ) {
			return false;
		}

		if ( ! self::fundraiser_is_open( $parent_id ) ) {
			return false;
		}

		// Personal fundraisers are leaf nodes — only roots and teams can be parents.
		$parent_post = get_post( $parent_id );
		if ( $parent_post && (int) $parent_post->post_parent > 0 && ! self::fundraiser_is_team( $parent_id ) ) {
			return false;
		}

		$new_depth = self::calculate_fundraiser_depth( $parent_id ) + 1;
		if ( $new_depth > self::MAX_DEPTH ) {
			return false;
		}

		// A user can only have one personal fundraiser under a given parent.
		$current_user_id = get_current_user_id();
		if ( $current_user_id > 0 && self::user_has_personal_fundraiser_under( $parent_id, $current_user_id ) ) {
			return false;
		}

		return true;
	}

	/**
	 * Returns true when the given user already has a personal (non-team) fundraiser directly under $parent_id.
	 *
	 * @param int $parent_id Parent fundraiser ID.
	 * @param int $user_id   WordPress user ID.
	 *
	 * @return bool
	 */
	public static function user_has_personal_fundraiser_under( int $parent_id, int $user_id ): bool {
		if ( $parent_id <= 0 || $user_id <= 0 ) {
			return false;
		}

		$existing = get_posts(
			array(
				'post_type'      => FundraisersModule::POST_TYPE,
				'post_parent'    => $parent_id,
				'author'         => $user_id,
				'post_status'    => array( 'publish', 'pending_verification' ),
				// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- already scoped by post_parent and author above; there's no non-meta way to distinguish member fundraisers from sub-teams.
				'meta_query'     => array(
					'relation' => 'OR',
					array(
						'key'     => FundraisersModule::META_IS_TEAM,
						'compare' => 'NOT EXISTS',
					),
					array(
						'key'     => FundraisersModule::META_IS_TEAM,
						'value'   => '1',
						'compare' => '!=',
					),
				),
				'posts_per_page' => 1,
				'fields'         => 'ids',
				'no_found_rows'  => true,
			)
		);

		return ! empty( $existing );
	}

	/**
	 * Returns true when creating a sub-fundraiser (non-team container) is allowed under the given parent.
	 *
	 * A sub-fundraiser must be able to have children, so it can only be created when the resulting
	 * depth leaves room for at least one more level (i.e. new depth < MAX_DEPTH).
	 *
	 * @param int $parent_id Parent fundraiser ID (0 = no parent).
	 *
	 * @return bool
	 */
	public static function sub_fundraiser_creation_allowed( int $parent_id = 0 ): bool {
		if ( $parent_id <= 0 ) {
			return false;
		}

		if ( ! self::fundraiser_is_active( $parent_id ) ) {
			return false;
		}

		if ( ! self::fundraiser_is_open( $parent_id ) ) {
			return false;
		}

		// Personal fundraisers are leaf nodes — only roots and teams can be parents.
		$parent_post = get_post( $parent_id );
		if ( $parent_post && (int) $parent_post->post_parent > 0 && ! self::fundraiser_is_team( $parent_id ) ) {
			return false;
		}

		$new_depth = self::calculate_fundraiser_depth( $parent_id ) + 1;
		if ( $new_depth >= self::MAX_DEPTH ) {
			return false;
		}

		return true;
	}

	// -------------------------------------------------------------------------
	// Hierarchy
	// -------------------------------------------------------------------------

	/**
	 * Calculate the absolute depth of a fundraiser (1 = root/top-level, 2 = one level down, etc.)
	 *
	 * Traverses the post_parent chain upward, capping at 10.
	 *
	 * @param int $fundraiser_id Fundraiser post ID.
	 *
	 * @return int Depth level (1-based).
	 */
	public static function calculate_fundraiser_depth( int $fundraiser_id ): int {
		$depth      = 1;
		$current_id = $fundraiser_id;

		while ( $current_id > 0 ) {
			$parent_id = (int) wp_get_post_parent_id( $current_id );

			if ( ! $parent_id ) {
				break;
			}

			++$depth;
			$current_id = $parent_id;

			if ( $depth > 10 ) {
				break;
			}
		}

		return $depth;
	}
}
