<?php
/**
 * String manipulation helper functions.
 *
 * @package Awesomemotive\Rewardswp\Helpers
 * @since 1.0.0
 */

namespace Awesomemotive\Rewardswp\Helpers;

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

use Awesomemotive\Rewardswp\Services\OptionsService;

use function Awesomemotive\Rewardswp\Helpers\is_rewardswp_pro_build;

/**
 * Generates a hash based on the input string.
 *
 * The hash is generated using SHA-256 to ensure uniqueness, and the length of the hash
 * can be controlled. If no length is passed, the default length will be 8 characters.
 * The function ensures the hash contains at least one letter to avoid all-numeric results.
 *
 * @since 1.0.0
 *
 * @param string|null $input  Optional. An input string to be hashed.
 * @param string      $prefix Optional. Prefix to prepend the hash to. Default is empty string.
 * @param int         $length Optional. The length of the hash. Default is 8.
 *
 * @return string The generated hash.
 */
function generate_custom_hash( ?string $input = null, string $prefix = '', int $length = 8 ): string {
	$final_length = $length > 0 ? $length : 8;

	// Get the full hash.
	$full_hash = hash(
		'sha256',
		( $input ?? microtime( true ) ) . wp_salt()
	);

	// Take the first portion based on the desired length.
	$hash_portion = substr( $full_hash, 0, $final_length );

	// Check if the hash could be interpreted as numeric (including scientific notation).
	// This covers pure digits, scientific notation (e.g., "123e456"), and other numeric formats.
	if ( ctype_digit( $hash_portion ) || is_numeric( $hash_portion ) ) {
		// Replace at least one digit with a letter from the latter part of the hash.
		$letter_position = wp_rand( 0, $final_length - 1 );
		$letter_source   = substr( $full_hash, $final_length, 6 ); // Get some characters from later in the hash.

		// Find the first letter in the source (avoiding 'e' to prevent scientific notation).
		preg_match( '/[a-df-z]/i', $letter_source, $matches );

		if ( ! empty( $matches ) ) {
			// Replace a random digit with the found letter.
			$hash_portion = substr_replace( $hash_portion, $matches[0], $letter_position, 1 );
		} else {
			// Fallback: use a hardcoded letter (avoiding 'e').
			$hash_portion = substr_replace( $hash_portion, 'g', $letter_position, 1 );
		}
	}

	return $prefix . $hash_portion;
}

/**
 * Helper function to replace placeholders like {name} in the content.
 *
 * @since 1.0.0
 *
 * @param string $template The content template.
 * @param array  $data     The data to replace placeholders.
 *
 * @return string The content with placeholders replaced.
 */
function replace_placeholders( string $template, array $data ): string {
	foreach ( $data as $key => $value ) {
		$template = str_replace( "{{$key}}", $value ?? '', $template );
	}
	return $template;
}

/**
 * Converts a snake_case string to PascalCase.
 *
 * @since 1.0.0
 *
 * @param string $string The snake_case string to convert.
 *
 * @return string The converted PascalCase string.
 */
function str_snake_to_pascal_case( string $string ): string {
	return str_replace( ' ', '', ucwords( str_replace( '_', ' ', $string ) ) );
}

/**
 * Converts a snake_case string to camelCase.
 *
 * @since 1.0.0
 *
 * @param string $string The snake_case string to convert.
 *
 * @return string The converted camelCase string.
 */
function str_snake_to_camel_case( string $string ): string {
	return lcfirst( str_snake_to_pascal_case( $string ) );
}

/**
 * Converts a kebab-case string to camelCase.
 *
 * @since 1.0.0
 *
 * @param string $string The kebab-case string to convert.
 *
 * @return string The converted camelCase string.
 */
function str_kebab_to_camel_case( string $string ): string {
	return lcfirst( str_replace( ' ', '', ucwords( str_replace( '-', ' ', $string ) ) ) );
}

/**
 * Converts an associative array to a JSON string, very handy for use when creating x-data.
 *
 * @since 1.0.0
 *
 * @param array $data       The array to convert.
 * @param bool  $hide_empty Whether to remove entries with empty values.
 * @param bool  $camel_case Whether the array keys should be converted to camelCase format.
 *
 * @return string JSON-encoded string.
 */
function array_to_json( array $data, bool $hide_empty = true, bool $camel_case = false ): string {
	// Filter out empty values if $hide_empty is true.
	if ( $hide_empty ) {
		$data = array_filter(
			$data,
			static function ( $value ) {
				return ! is_null( $value ) && '' !== $value;
			}
		);
	}

	$to_camel = function ( $str ) {
		return str_kebab_to_camel_case(
			str_snake_to_camel_case( $str )
		);
	};

	// Convert array keys to camelCase if $camel_case is true.
	if ( $camel_case ) {
		$data = array_combine(
			array_map( $to_camel, array_keys( $data ) ),
			$data
		);
	}

	// Convert the array to a JSON string and escape for safe output.
	return htmlspecialchars(
		wp_json_encode( $data, JSON_HEX_TAG ),
		ENT_QUOTES,
		'UTF-8'
	);
}

/**
 * Generates an HTML tag attribute as a string.
 *
 * This function creates an HTML attribute as a string with the provided name and value.
 * By default, if the value is empty, the attribute will not be included in the output.
 * This is particularly useful for avoiding unnecessary HTML generation when attribute
 * values may be empty or conditional.
 *
 * @since 1.0.0
 *
 * @param string $name       Attribute name
 * @param mixed  $value      Attribute value. Arrays will be automatically converted to JSON.
 * @param bool   $hide_empty Do not print the attribute if $value is empty.
 * @param bool   $echo       Whether echo or return the output.
 */
function tag_attr( string $name, $value, bool $hide_empty = true, bool $echo = true ) {

	if ( $hide_empty && empty( $value ) ) {
		return '';
	}

	$tag = sprintf(
		' %s="%s"',
		esc_attr( $name ),
		is_array( $value )
			? esc_attr( wp_json_encode( $value ) )
			: esc_attr( $value )
	);

	if ( ! $echo ) {
		return $tag;
	}

	echo wp_kses_data( $tag );
}

/**
 * Build the input name attribute.
 *
 * @since 1.0.0
 *
 * @param string $group The group name.
 * @param string $name  The specific name.
 *
 * @return string The generated input name.
 */
function build_input_name( string $group, string $name ): string {
	return sprintf(
		'%s[%s][%s]',
		OptionsService::OPTION_NAME,
		$group,
		$name
	);
}

/**
 * Build the input ID attribute.
 *
 * @since 1.0.0
 *
 * @param string $group The group name.
 * @param string $name  The specific name.
 * @param string $extra Additional string to add to the end of the ID, if provided.
 *
 * @return string The generated input ID.
 */
function build_input_id( string $group, string $name, string $extra = '' ): string {
	$id = sprintf(
		'%s_%s_%s',
		OptionsService::OPTION_NAME,
		$group,
		$name
	);

	return empty( $extra )
		? $id
		: "{$id}_{$extra}";
}

/**
 * Parse an input name and return the group and name.
 *
 * @since 1.0.0
 *
 * @param string $input_name The input name to parse.
 *
 * @return array An associative array with 'group' and 'name'.
 */
function parse_input_name( string $input_name ): array {
	$matches = [];

	// Match the format rewardswp[group][name].
	if ( preg_match( '/^([\w]+)\[([\w]+)\]\[([\w]+)\]$/', $input_name, $matches ) ) {
		return [
			'group' => $matches[2],
			'name'  => $matches[3],
		];
	}

	// If no match, treat it as a standalone name.
	return [
		'group' => null,
		'name'  => $input_name,
	];
}

/**
 * Parses a user's name, returning the first and last names in an array.
 *
 * @since 1.0.0
 *
 * @param string $name
 *
 * @return array
 */
function parse_name( string $name ): array {
	$names      = explode( ' ', $name );
	$first_name = ! empty( $names[0] ) ? $names[0] : '';
	$last_name  = '';

	if ( ! empty( $names[1] ) ) {
		unset( $names[0] );
		$last_name = implode( ' ', $names );
	}

	return [
		'first_name' => $first_name,
		'last_name'  => $last_name,
	];
}

/**
 * Format a full name to show first name + initial of last name.
 *
 * Examples:
 * - "João Silva" -> "João S."
 * - "Maria da Silva" -> "Maria S."
 * - "Pedro" -> "Pedro"
 *
 * @since 1.2.0
 *
 * @param string $full_name The full name to format.
 *
 * @return string Formatted name with first name + initial, or empty string if name is empty.
 */
function format_name_first_and_initial( string $full_name ): string {
	if ( empty( trim( $full_name ) ) ) {
		return '';
	}

	$parsed = parse_name( trim( $full_name ) );
	$first_name = $parsed['first_name'] ?? '';
	$last_name  = $parsed['last_name'] ?? '';

	// If no last name, return just the first name.
	if ( empty( $last_name ) ) {
		return $first_name;
	}

	// Get first character of last name (handle multi-word last names by getting first char of first word).
	$last_name_words = explode( ' ', trim( $last_name ) );
	$initial = ! empty( $last_name_words[0] ) ? mb_substr( $last_name_words[0], 0, 1 ) : '';

	return $first_name . ( ! empty( $initial ) ? ' ' . mb_strtoupper( $initial ) . '.' : '' );
}

/**
 * Return a constant value if exists, with a fallback option.
 *
 * @since 1.0.0
 *
 * @param string $name    The constant name.
 * @param mixed  $default Fallback value in case the const is not defined.
 *
 * @return mixed|null
 */
function get_constant_value_if_defined( string $name, $default = null ) {
	return defined( $name ) ? constant( $name ) : $default;
}

/**
 * Extract placeholders from content string.
 *
 * Finds all placeholders in the format {placeholder_name} and returns them as an array.
 * Placeholders can contain letters, numbers, and underscores.
 *
 * @since 1.0.0
 *
 * @param string $content The content to extract placeholders from.
 *
 * @return array Array of unique placeholder names (without the braces).
 */
function extract_placeholders( string $content ): array {
	$matches = [];

	// Match placeholders in the format {placeholder_name}
	// Allows letters, numbers, underscores in placeholder names
	preg_match_all( '/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', $content, $matches );

	// Return unique placeholder names (without the braces)
	return array_unique( $matches[1] ?? [] );
}

/**
 * Get placeholders from content with their display labels.
 *
 * Extracts placeholders and returns them with formatted display labels.
 * Converts snake_case to Title Case for better readability.
 *
 * @since 1.0.0
 *
 * @param string $content The content to extract placeholders from.
 *
 * @return array Associative array with placeholder => label pairs.
 */
function get_placeholders_with_labels( string $content ): array {
	$placeholders = extract_placeholders( $content );
	$labeled      = [];

	foreach ( $placeholders as $placeholder ) {
		// Convert snake_case to Title Case for display
		$label                   = ucwords( str_replace( '_', ' ', $placeholder ) );
		$labeled[ $placeholder ] = $label;
	}

	return $labeled;
}

/**
 * Generate HTML tooltip content from an array of paragraphs.
 *
 * Supports basic HTML formatting like <strong> tags within paragraphs.
 *
 * @since 1.1.0
 *
 * @param array       $paragraphs     Array of paragraph strings (supports basic HTML like <strong>).
 * @param string|null $learn_more_url Optional URL for a "Learn more" link.
 * @param string      $tooltip_id     Identifier for this tooltip (e.g., 'member-growth-chart').
 *
 * @return string HTML content for tooltip.
 */
function tooltip( array $paragraphs, ?string $learn_more_url = null, string $tooltip_id = '' ): string {
	$html = '';

	foreach ( $paragraphs as $paragraph ) {
		$html .= '<p>' . wp_kses_post( $paragraph ) . '</p>';
	}

	if ( $learn_more_url ) {
		// Campaign is 'liteplugin' for lite build users, 'plugin' for pro build users.
		$campaign = is_rewardswp_pro_build() ? 'plugin' : 'liteplugin';

		// Auto-detect medium from current admin page and tab (e.g., 'rewardswp-reports' + 'export' -> 'reports-export').
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$page   = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : '';
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$tab    = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : '';
		$medium = str_replace( 'rewardswp-', '', $page ) . ( $tab ? '-' . $tab : '' );

		// Build UTM params.
		$url = add_query_arg(
			[
				'utm_campaign' => $campaign,
				'utm_source'   => 'WordPress',
				'utm_medium'   => $medium,
				'utm_content'  => sanitize_key( $tooltip_id ),
				'utm_locale'   => get_locale(),
			],
			$learn_more_url
		);

		$html .= '<p class="mt-2"><a href="' . esc_url( $url ) . '" target="_blank" rel="noopener" class="text-blue-500 hover:text-blue-600 underline underline-offset-2">' . __( 'Learn more', 'rewardswp' ) . '</a></p>';
	}

	return $html;
}

/**
 * Mask an email address for privacy display.
 *
 * Shows a portion of the local part (before @) followed by asterisks,
 * while hiding the domain part completely. This helps users identify
 * the email owner without exposing the full address.
 *
 * @since 1.2.0
 *
 * @param string $email           The email address to mask.
 * @param int    $visible_chars   Optional. Number of characters from the local part to show. Default 6.
 * @param string $mask_char       Optional. Character to use for masking. Default '*'.
 * @param int    $mask_length     Optional. Number of mask characters to append. Default 3.
 *
 * @return string The masked email address (e.g., 'darvin***' or 'johnsm***').
 */
function mask_email( string $email ): string {
    if ( ! filter_var( $email, FILTER_VALIDATE_EMAIL ) ) {
        return '';
    }

    [ $local, $domain ] = explode( '@', $email, 2 );

    // Mask the local part (before @).
    // Cap asterisks at 5 regardless of actual length (industry standard: Google, Apple, etc.).
    $local_length = strlen( $local );

    if ( $local_length <= 2 ) {
        $masked_local = str_repeat( '*', $local_length );
    } else {
        $mask_length  = min( $local_length - 2, 5 );
        $masked_local =
            substr( $local, 0, 1 ) .
            str_repeat( '*', $mask_length ) .
            substr( $local, -1 );
    }

    return $masked_local . '@' . $domain;
}
