<?php
/**
 * Helper Class - Utility functions for SureDonation
 *
 * @package SureDonation
 */

namespace SureDonation\Inc;

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

/**
 * Helper class.
 * Provides utility functions for the plugin.
 *
 * @since 0.0.1
 */
class Helper {
	/**
	 * Option name for all SureDonation settings.
	 *
	 * @since 0.0.1
	 */
	public const OPTION_NAME = 'suredonation_options';

	/**
	 * Campaign meta key name.
	 *
	 * @since 0.0.1
	 */
	public const SUREDONATION_CAMPAIGN_META_KEY = '_suredonation_campaign_meta';

	/**
	 * Default campaign meta values.
	 *
	 * @since 0.0.1
	 * @var array<string, mixed>
	 */
	private static $campaign_meta_defaults = [
		'goal_type'           => 'raised_amount',
		'goal_amount'         => 0,
		'campaign_status'     => 'active',
		'email_settings'      => [],
		'allow_fees_coverage' => false,
		'require_terms'       => false,
		'terms_text'          => '',
		'thank_you_message'   => '',
	];

	/**
	 * Get a value from the suredonation_options array.
	 *
	 * @param string $key           The key to retrieve.
	 * @param mixed  $default_value Default value if key doesn't exist.
	 * @return mixed
	 * @since 0.0.1
	 */
	public static function get_suredonation_option( $key, $default_value = null ) {
		$options = get_option( self::OPTION_NAME, [] );

		if ( ! is_array( $options ) ) {
			$options = [];
		}

		return array_key_exists( $key, $options ) ? $options[ $key ] : $default_value;
	}

	/**
	 * Update a value in the suredonation_options array.
	 *
	 * @param string $key   The key to update.
	 * @param mixed  $value The value to set.
	 * @return bool True on success, false on failure.
	 * @since 0.0.1
	 */
	public static function update_suredonation_option( $key, $value ) {
		$options = get_option( self::OPTION_NAME, [] );

		if ( ! is_array( $options ) ) {
			$options = [];
		}

		$options[ $key ] = $value;

		return update_option( self::OPTION_NAME, $options );
	}

	/**
	 * Get all campaign meta as an array.
	 *
	 * @param int $campaign_id Campaign post ID.
	 * @return array<string, mixed> Campaign meta values.
	 * @since 0.0.1
	 */
	public static function get_campaign_meta( $campaign_id ) {
		$raw = get_post_meta( $campaign_id, self::SUREDONATION_CAMPAIGN_META_KEY, true );

		$meta = ! empty( $raw ) && is_string( $raw ) ? json_decode( $raw, true ) : [];

		if ( ! is_array( $meta ) ) {
			$meta = [];
		}

		return array_merge( self::$campaign_meta_defaults, $meta );
	}

	/**
	 * Get a single campaign meta value.
	 *
	 * @param int    $campaign_id Campaign post ID.
	 * @param string $key         Meta key within the campaign meta array.
	 * @param mixed  $default_value     Default value if not set.
	 * @return mixed
	 * @since 0.0.1
	 */
	public static function get_campaign_meta_value( $campaign_id, $key, $default_value = null ) {
		$meta = self::get_campaign_meta( $campaign_id );

		return $meta[ $key ] ?? $default_value;
	}

	/**
	 * Update campaign meta. Merges provided values with existing meta.
	 *
	 * @param int                  $campaign_id Campaign post ID.
	 * @param array<string, mixed> $values      Key-value pairs to update.
	 * @return bool|int Meta ID on success, false on failure.
	 * @since 0.0.1
	 */
	public static function update_campaign_meta( $campaign_id, $values ) {
		$meta = self::get_campaign_meta( $campaign_id );
		$meta = array_merge( $meta, $values );

		return update_post_meta( $campaign_id, self::SUREDONATION_CAMPAIGN_META_KEY, wp_json_encode( $meta ) );
	}

	/**
	 * Checks if current value is string or else returns default value
	 *
	 * @param mixed $data data which need to be checked if is string.
	 * @return string
	 * @since 0.0.1
	 */
	public static function get_string_value( $data ) {
		if ( is_scalar( $data ) ) {
			return (string) $data;
		}
		if ( is_object( $data ) && method_exists( $data, '__toString' ) ) {
			return $data->__toString();
		}
		if ( is_null( $data ) ) {
			return '';
		}
			return '';
	}

	/**
	 * Checks if current value is number or else returns default value
	 *
	 * @param mixed $value data which need to be checked if is string.
	 * @param int   $base value can be set is $data is not a string, defaults to empty string.
	 * @return int
	 * @since 0.0.1
	 */
	public static function get_integer_value( $value, $base = 10 ) {
		if ( is_numeric( $value ) ) {
			return (int) $value;
		}
		if ( is_string( $value ) ) {
			$trimmed_value = trim( $value );
			return intval( $trimmed_value, $base );
		}
			return 0;
	}

	/**
	 * Safely converts a mixed value to float
	 *
	 * @param mixed $value         The value to convert.
	 * @param float $default_value Default value if conversion fails.
	 * @return float
	 * @since 0.0.1
	 */
	public static function get_float_value( $value, $default_value = 0.0 ) {
		if ( is_numeric( $value ) ) {
			return (float) $value;
		}
		return $default_value;
	}

	/**
	 * Safely get array value with type checking
	 *
	 * @param mixed                $value         The value to check.
	 * @param array<string, mixed> $default_value Default value if not an array.
	 * @return array<string, mixed>
	 * @since 0.0.1
	 */
	public static function get_array_value( $value, $default_value = [] ) {
		return is_array( $value ) ? $value : $default_value;
	}

	/**
	 * Check if current user has required capability.
	 *
	 * @param string       $capability Capability to check (default: 'manage_options').
	 * @param array<mixed> $args Additional arguments for capability check.
	 * @return bool True if user has capability.
	 * @since 0.0.1
	 */
	public static function current_user_can( $capability = '', $args = [] ) {
		if ( ! function_exists( 'current_user_can' ) ) {
			return false;
		}

		if ( ! is_string( $capability ) || empty( $capability ) ) {
			$capability = 'manage_options';
		}

		return ! empty( $args )
			? current_user_can( $capability, ...$args )
			: current_user_can( $capability );
	}

	/**
	 * Join an array of strings into a single string, filtering out empty values.
	 *
	 * @param array<string> $strings Array of strings to join.
	 * @param string        $glue    Separator to use (default: ' ').
	 * @return string Joined string.
	 * @since 0.0.1
	 */
	public static function join_strings( $strings, $glue = ' ' ) {
		if ( ! is_array( $strings ) ) {
			return '';
		}

		$filtered = array_filter(
			$strings,
			static function ( $item ) {
				return is_string( $item ) && '' !== trim( $item );
			}
		);

		return implode( $glue, array_map( 'trim', $filtered ) );
	}

	/**
	 * Process blocks to generate unique slugs for SureDonation blocks.
	 *
	 * Recursively processes all blocks and generates slugs for those that
	 * don't have one set. Ensures all slugs are unique within the form.
	 *
	 * @param array<mixed>  $blocks  The blocks to process.
	 * @param array<string> $slugs   Array of existing slugs (keyed by block_id).
	 * @param bool          $updated Whether any blocks were updated.
	 * @param string        $prefix  Optional prefix for nested blocks.
	 * @return array{0: array<mixed>, 1: array<string>, 2: bool} Processed blocks, slugs, and updated flag.
	 * @since 0.0.1
	 */
	public static function process_blocks( $blocks, $slugs = [], $updated = false, $prefix = '' ) {
		if ( ! is_array( $blocks ) ) {
			return [ [], $slugs, $updated ];
		}
		foreach ( $blocks as $index => $block ) {
			if ( ! is_array( $block ) ) {
				continue;
			}
			// Skip non-SureDonation blocks.
			if ( ! isset( $block['blockName'] ) || ! is_string( $block['blockName'] ) || strpos( $block['blockName'], 'sd/' ) !== 0 ) {
				// Process inner blocks if any.
				if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
					[ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, $prefix );
				}
				continue;
			}

			// Skip if no attrs or slug is already set and block_id is in slugs array.
			if (
				! isset( $block['attrs'] ) ||
				! is_array( $block['attrs'] ) ||
				(
					! empty( $block['attrs']['slug'] ) &&
					isset( $block['attrs']['block_id'] ) &&
					isset( $slugs[ $block['attrs']['block_id'] ] )
				)
			) {
				// Process inner blocks if any.
				if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
					[ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, $prefix );
				}
				continue;
			}

			// Generate slug if empty.
			if ( empty( $block['attrs']['slug'] ) ) {
				$blocks[ $index ]['attrs']['slug'] = self::generate_unique_block_slug( $block, $slugs, $prefix );
				$updated                           = true;
			}

			// Track the slug if block_id is set.
			if ( isset( $block['attrs']['block_id'] ) ) {
				$slugs[ $block['attrs']['block_id'] ] = $blocks[ $index ]['attrs']['slug'];
			}

			// Process inner blocks recursively.
			if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
				[ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks(
					$block['innerBlocks'],
					$slugs,
					$updated,
					$blocks[ $index ]['attrs']['slug']
				);
			}
		}

		return [ $blocks, $slugs, $updated ];
	}

	/**
	 * Generates a unique slug based on the provided block and existing slugs.
	 *
	 * @param array<mixed>  $block  The block data.
	 * @param array<string> $slugs  The array of existing slugs.
	 * @param string        $prefix Optional prefix for nested blocks.
	 * @return string The generated unique block slug.
	 * @since 0.0.1
	 */
	public static function generate_unique_block_slug( $block, $slugs, $prefix = '' ) {
		$slug = is_string( $block['blockName'] ?? '' ) ? str_replace( 'sd/', '', $block['blockName'] ) : '';

		// Use label if available.
		if ( ! empty( $block['attrs']['label'] ) && is_string( $block['attrs']['label'] ) ) {
			$slug = sanitize_title( $block['attrs']['label'] );
		}

		// Add prefix for nested blocks.
		if ( ! empty( $prefix ) ) {
			$slug = $prefix . '-' . $slug;
		}

		return self::generate_unique_slug( $slug, $slugs );
	}

	/**
	 * Ensures that the slug is unique.
	 *
	 * If the slug is already taken, it appends a number to make it unique.
	 *
	 * @param string        $slug  The slug to make unique.
	 * @param array<string> $slugs Array of existing slugs.
	 * @return string The unique slug.
	 * @since 0.0.1
	 */
	public static function generate_unique_slug( $slug, $slugs ) {
		$slug = sanitize_title( $slug );

		// Check if slug exists in the array values.
		if ( ! in_array( $slug, $slugs, true ) ) {
			return $slug;
		}

		// Append a number to make it unique.
		$index = 1;
		while ( in_array( $slug . '-' . $index, $slugs, true ) ) {
			++$index;
		}

		return $slug . '-' . $index;
	}

	/**
	 * Get client IP address for logging purposes.
	 *
	 * Checks forwarded headers first (for proxied/load-balanced environments)
	 * then falls back to REMOTE_ADDR. This is suitable for informational
	 * logging only — do NOT use for security-critical IP validation.
	 *
	 * @return string Client IP address.
	 * @since 0.0.1
	 */
	public static function get_client_ip() {
		$ip_headers = [
			'HTTP_CLIENT_IP',
			'HTTP_X_FORWARDED_FOR',
			'REMOTE_ADDR',
		];

		foreach ( $ip_headers as $header ) {
			if ( ! empty( $_SERVER[ $header ] ) ) {
				$ips = explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) );
				$ip  = trim( $ips[0] );

				if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
					return $ip;
				}
			}
		}

		return '';
	}

	/**
	 * Get allowed HTML tags for form markup.
	 *
	 * The wp_kses_post() doesn't allow form elements, so we need a custom allowed tags array.
	 * This is safe because the markup is generated internally by trusted code that already
	 * escapes user input with esc_attr(), esc_html(), etc.
	 *
	 * @return array<string, array<string, bool>> Allowed HTML tags and attributes.
	 * @since 0.0.1
	 */
	public static function get_allowed_form_html() {
		// Note: data-* wildcard doesn't work in wp_kses, so we list each data attribute explicitly.
		$common_data_attrs = [
			'data-block-id'                    => true,
			'data-form-id'                     => true,
			'data-gateway'                     => true,
			'data-stripe-key'                  => true,
			'data-currency'                    => true,
			'data-payment-mode'                => true,
			'data-amount-type'                 => true,
			'data-fixed-amount'                => true,
			'data-payment-type'                => true,
			'data-customer-name-field'         => true,
			'data-customer-email-field'        => true,
			'data-nonce'                       => true,
			'data-variable-amount-field'       => true,
			'data-minimum-amount'              => true,
			'data-subscription-plan-name'      => true,
			'data-subscription-interval'       => true,
			'data-subscription-billing-cycles' => true,
			'data-currency-symbol'             => true,
			'data-message-format'              => true,
			'data-slug'                        => true,
			'data-required'                    => true,
			'data-fee-percentage'              => true,
			'data-fee-fixed'                   => true,
			'data-invalid-email-msg'           => true,
		];

		return [
			'div'      => array_merge(
				[
					'id'              => true,
					'class'           => true,
					'style'           => true,
					'role'            => true,
					'aria-live'       => true,
					'aria-atomic'     => true,
					'aria-labelledby' => true,
				],
				$common_data_attrs
			),
			'form'     => array_merge(
				[
					'id'     => true,
					'class'  => true,
					'method' => true,
					'action' => true,
				],
				$common_data_attrs
			),
			'fieldset' => [
				'id'    => true,
				'class' => true,
			],
			'legend'   => [
				'id'    => true,
				'class' => true,
			],
			'label'    => [
				'id'    => true,
				'class' => true,
				'for'   => true,
			],
			'input'    => array_merge(
				[
					'id'               => true,
					'class'            => true,
					'type'             => true,
					'name'             => true,
					'value'            => true,
					'placeholder'      => true,
					'min'              => true,
					'max'              => true,
					'step'             => true,
					'maxlength'        => true,
					'checked'          => true,
					'disabled'         => true,
					'readonly'         => true,
					'required'         => true,
					'aria-describedby' => true,
					'aria-required'    => true,
					'aria-hidden'      => true,
				],
				$common_data_attrs
			),
			'button'   => array_merge(
				[
					'id'       => true,
					'class'    => true,
					'type'     => true,
					'disabled' => true,
				],
				$common_data_attrs
			),
			'select'   => array_merge(
				[
					'id'               => true,
					'class'            => true,
					'name'             => true,
					'disabled'         => true,
					'required'         => true,
					'aria-describedby' => true,
					'aria-required'    => true,
				],
				$common_data_attrs
			),
			'option'   => [
				'value'    => true,
				'selected' => true,
				'disabled' => true,
			],
			'textarea' => array_merge(
				[
					'id'               => true,
					'class'            => true,
					'name'             => true,
					'rows'             => true,
					'cols'             => true,
					'placeholder'      => true,
					'maxlength'        => true,
					'disabled'         => true,
					'readonly'         => true,
					'required'         => true,
					'aria-describedby' => true,
					'aria-required'    => true,
				],
				$common_data_attrs
			),
			'span'     => [
				'id'          => true,
				'class'       => true,
				'style'       => true,
				'aria-hidden' => true,
			],
			'p'        => [
				'id'    => true,
				'class' => true,
				'style' => true,
				'role'  => true,
			],
			'a'        => [
				'id'     => true,
				'class'  => true,
				'href'   => true,
				'target' => true,
				'rel'    => true,
				'style'  => true,
			],
			'strong'   => [
				'class' => true,
			],
			'em'       => [
				'class' => true,
			],
			'br'       => [],
			'svg'      => [
				'class'       => true,
				'width'       => true,
				'height'      => true,
				'viewbox'     => true,
				'fill'        => true,
				'xmlns'       => true,
				'aria-hidden' => true,
			],
			'circle'   => [
				'cx'           => true,
				'cy'           => true,
				'r'            => true,
				'stroke'       => true,
				'stroke-width' => true,
				'fill'         => true,
			],
			'rect'     => [
				'x'            => true,
				'y'            => true,
				'width'        => true,
				'height'       => true,
				'rx'           => true,
				'stroke'       => true,
				'stroke-width' => true,
			],
			'path'     => [
				'class'           => true,
				'd'               => true,
				'stroke'          => true,
				'stroke-width'    => true,
				'stroke-linecap'  => true,
				'stroke-linejoin' => true,
				'fill'            => true,
			],
		];
	}
}
