<?php

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

use Gigneo\Modules;
use Gigneo\Notifications;
use Gigneo\Post_Types;

/**
 * Retrieve the Gigneo template path.
 *
 * This function checks for the existence of the Gigneo template in various locations
 * and returns the appropriate path.
 *
 * @param string $template The template name.
 *
 * @return string The template path if found, false otherwise.
 * @since 0.0.1
 */
function gigneo_get_template( $template ) {
	$template = preg_replace( '/\.php$/', '', $template );
	if ( file_exists( get_stylesheet_directory() . '/gigneo/' . $template . '.php' ) ) {
		$template = get_stylesheet_directory() . '/gigneo/' . $template . '.php';
	} elseif ( file_exists( get_template_directory() . '/gigneo/' . $template . '.php' ) ) {
		$template = get_template_directory() . 'gigneo/' . $template . '.php';
	} elseif ( did_action( 'gigneo_pro_loaded' ) && file_exists( GIGNEO_PRO_DIR_PATH . 'templates/' . $template . '.php' ) ) {
		$template = GIGNEO_PRO_DIR_PATH . 'templates/' . $template . '.php';
	} else {
		$template = GIGNEO_DIR_PATH . 'templates/' . $template . '.php';
	}

	return apply_filters( 'gigneo_get_template', $template );
}

/**
 * Load a Gigneo template.
 *
 * This function loads the specified Gigneo template file.
 *
 * @param string $template The template name.
 * @param bool $once Whether to include the template file only once. Default is true.
 *
 * @since 0.0.1
 */
function gigneo_load_template( $template, $once = true, $args = array() ) {
	$template = gigneo_get_template( $template );

	$args = is_array( $args ) ? $args : array();
	// phpcs:ignore WordPress.PHP.DontExtract.extract_extract -- Template variables are expected to be in local scope (WordPress pattern).
	extract( $args, EXTR_SKIP );

	if ( $once ) {
		require_once $template;
	} else {
		require $template;
	}
}

/**
 * Retrieve a setting option.
 *
 * Values are cached in $gigneo_options for the current request so repeated
 * lookups for the same key do not hit Carbon Fields / the database again.
 *
 * @param string $key The key of the setting option.
 * @param mixed $default_value The default value to return if the setting option is not found.
 *
 * @return mixed The value of the setting option or the default value.
 * @since 0.0.1
 */
function gigneo_option( $key, $default_value = '' ) {
	global $gigneo_options;

	if ( ! is_array( $gigneo_options ) ) {
		$gigneo_options = array();
	}

	if ( array_key_exists( $key, $gigneo_options ) ) {
		$setting_option = $gigneo_options[ $key ];
	} else {
		$option_key     = str_starts_with( $key, '_' ) ? substr( $key, 1 ) : $key;
		$setting_option = null;

		if ( function_exists( 'carbon_get_theme_option' ) ) {
			$setting_option = carbon_get_theme_option( $option_key );
		}

		if ( empty( $setting_option ) ) {
			$setting_option = get_option( $key, $default_value );
		}

		$gigneo_options[ $key ] = $setting_option;
	}

	if ( empty( $setting_option ) ) {
		return apply_filters( "gigneo_option_$key", $default_value );
	}

	return apply_filters( "gigneo_option_$key", $setting_option );
}

/**
 * Clear the request-level gigneo_option() cache.
 *
 * Call this after theme options are saved so subsequent reads get fresh values.
 *
 * @return void
 * @since 0.0.1
 */
function gigneo_flush_option_cache() {
	global $gigneo_options;

	$gigneo_options = array();
}

/**
 * Retrieve post meta for a given post ID and key.
 *
 * @param int $post_id The ID of the post.
 * @param string $key The meta key to retrieve.
 *
 * @return mixed The post meta value or default.
 * @since 0.0.1
 */
function gigneo_post_meta( $post_id, $key ) {

	$meta = get_post_meta( $post_id, $key, true );

	return apply_filters( "gigneo_post_meta_$key", $meta, $post_id, $key );
}

/**
 * Retrieve term meta for a given term ID and key.
 *
 * @param int $term_id The ID of the term.
 * @param string $key The meta key to retrieve.
 *
 * @return mixed The term meta value or default.
 * @since 0.0.1
 */
function gigneo_term_meta( $term_id, $key ) {

	$option_key = str_starts_with( $key, '_' ) ? substr( $key, 1 ) : $key;

	$meta = get_term_meta( $term_id, $key, true );

	return apply_filters( "gigneo_term_meta_$key", $meta, $term_id, $key );
}

/**
 * Retrieve the plural form of the freelancer label.
 *
 * This function returns the plural form of the freelancer label, allowing for customization through filters.
 *
 * @return string The plural form of the freelancer label.
 * @since 0.0.1
 */
function gigneo_freelancer_plural() {
    $default = apply_filters( 'gigneo_freelancer_plural', esc_html__( 'Freelancers', 'gigneo' ) );

    return gigneo_option( '_gigneo_ts_post_type_freelancer_plural', $default );
}

/**
 * Retrieve the singular form of the freelancer label.
 *
 * This function returns the singular form of the freelancer label, allowing for customization through filters.
 *
 * @return string The singular form of the freelancer label.
 * @since 0.0.1
 */
function gigneo_freelancer_singular() {
    $default = apply_filters( 'gigneo_freelancer_singular', esc_html__( 'Freelancer', 'gigneo' ) );

    return gigneo_option( '_gigneo_ts_post_type_freelancer_singular', $default );
}

/**
 * Retrieve the plural form of the employer label.
 *
 * This function returns the plural form of the employer label, allowing for customization through filters.
 *
 * @return string The plural form of the employer label.
 * @since 0.0.1
 */
function gigneo_employer_plural() {
    $default = apply_filters( 'gigneo_employer_plural', esc_html__( 'Employers', 'gigneo' ) );

    return gigneo_option( '_gigneo_ts_post_type_employer_plural', $default );
}

/**
 * Retrieve the singular form of the employer label.
 *
 * This function returns the singular form of the employer label, allowing for customization through filters.
 *
 * @return string The singular form of the employer label.
 * @since 0.0.1
 */
function gigneo_employer_singular() {
    $default = apply_filters( 'gigneo_employer_singular', esc_html__( 'Employer', 'gigneo' ) );

    return gigneo_option( '_gigneo_ts_post_type_employer_singular', $default );
}

/**
 * Parse URL-encoded form data into an associative array.
 *
 * @param string $data URL-encoded form data string (e.g. "key=value&key2=value2").
 *
 * @return array Parsed form data as key => value pairs.
 * @since 0.0.1
 */
function gigneo_parse_form_data( $data ) {

	$form_data = array();

	if ( ! empty( $data ) && is_string( $data ) ) {
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Serialized form data is sanitized by callers at point of use.
		parse_str( wp_unslash( $data ), $form_data );
	}

	return is_array( $form_data ) ? $form_data : array();
}

/**
 * Retrieve the allowed countries based on location settings.
 *
 * This function returns an array of allowed countries depending on the location setting.
 *
 * @return array The array of allowed countries.
 * @since 0.0.1
 */
function gigneo_allowed_countries() {
	$type              = gigneo_option( '_gigneo_ts_countries', 'specific' );
	$allowed_countries = function_exists( 'WC' ) && WC()->countries ? WC()->countries->get_countries() : array();

	if ( 'all_except' === $type ) {
		$except            = gigneo_option( '_gigneo_ts_except_countries', array() );
		$allowed_countries = array_diff_key( $allowed_countries, array_flip( (array) $except ) );
	} elseif ( 'specific' === $type ) {
		$specific          = gigneo_option( '_gigneo_ts_specific_countries', array( 'US' ) );
		$allowed_countries = array_intersect_key( $allowed_countries, array_flip( (array) $specific ) );
	}

	return apply_filters( 'gigneo_allowed_countries', $allowed_countries, $type );
}

/**
 * Retrieve allowed states for the site's allowed countries.
 *
 * Returns either a flat list of states with keys in the format `COUNTRY:STATE`
 * or a grouped array by country name when `$group` is true.
 *
 * @param bool $group Whether to group states by country name. Default false.
 * @return array List of allowed states (flat or grouped).
 * @since 0.0.1
 */
function gigneo_allowed_states( $group = false ) {
	$states_list = array();

	// Get allowed countries
	$allowed_countries = gigneo_allowed_countries();

	foreach ( $allowed_countries as $country_key => $country_name ) {
		$states = function_exists( 'WC' ) && WC()->countries ? WC()->countries->get_states( $country_key ) : array();

		if ( ! empty( $states ) ) {
			foreach ( $states as $state_key => $state_name ) {
				if ( $group ) {
					// Grouped by country name
					$states_list[ $country_name ][ "{$country_key}:{$state_key}" ] = $state_name;
				} else {
					// Flat list
					$states_list[ "{$country_key}:{$state_key}" ] = $state_name;
				}
			}
		}
	}

	return apply_filters( 'gigneo_allowed_states', $states_list );
}

/**
 * Get countries with states.
 *
 * This function returns an array of countries with their corresponding states.
 *
 * @param string $type The type of countries to return.
 * @param bool $group Whether to group the countries with their states.
 *
 * @return array The array of countries with states.
 * @since 0.0.1
 */
function gigneo_countries_with_states( $type = '', $group = false, $none = true ) {

	$country_state = array();

	if ( $none ) {
		$country_state['none'] = esc_html__( 'None', 'gigneo' );
	}

	$counties = 'active' === $type ? gigneo_allowed_countries() : WC()->countries->get_countries();

	foreach ( $counties as $key => $country ) {
		$states = WC()->countries->get_states( $key );

		if ( $states ) {

			foreach ( $states as $state_key => $state ) {
				if ( $group ) {
					$country_state[ $country ][ "{$key}:{$state_key}" ] = $state . ', ' . $country;
				} else {
					$country_state[ "{$key}:{$state_key}" ] = $state . ', ' . $country;
				}
			}
		} else {
			$country_state[ $key ] = $country;
		}
	}

	return apply_filters( 'gigneo_countries_with_state', $country_state );
}

/**
 * Get cities based on country and state.
 *
 * @param string $country_state The country and state identifier.
 * @return array The array of cities.
 * @since 0.0.1
 */
function gigneo_cities( $country_state = '', $none = true ) {

	$args = array(
		'taxonomy'   => 'gigneo_city',
		'hide_empty' => false,
	);

	if ( ! empty( $country_state ) ) {
		// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		$args['meta_query'] = array(
			array(
				'key'     => '_gigneo_tm_country_state',
				'value'   => $country_state,
				'compare' => '=',
			),
		);
		// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
	}

	$cities     = array();
	$term_query = new WP_Term_Query( $args );

	if ( $none ) {
		$cities['none'] = esc_html__( 'None', 'gigneo' );
	}

	if ( ! empty( $term_query->terms ) ) {
		foreach ( $term_query->terms as $term ) {
			$cities[ $term->slug ] = $term->name;
		}
	}

	if ( ! empty( $country_state ) ) {
		return apply_filters( "gigneo_{$country_state}_cities", $cities );
	}

	return apply_filters( 'gigneo_cities', $cities );
}

/**
 * Get cities based on country and state key.
 *
 * This function retrieves cities based on country and state key.
 *
 * @param string $country_state The country and state identifier.
 * @return array The array of cities grouped by country and state.
 * @since 0.0.1
 */
function gigneo_country_state_cities( $country_state = '', $none = true ) {

	$args = array(
		'taxonomy'   => 'gigneo_city',
		'hide_empty' => false,
	);

	if ( ! empty( $country_state ) ) {
		// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		$args['meta_query'] = array(
			array(
				'key'     => '_gigneo_tm_country_state',
				'value'   => $country_state,
				'compare' => '=',
			),
		);
		// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
	}

	$cities     = array();
	$term_query = new WP_Term_Query( $args );

	// "None" is a flat-list option only; grouped mode uses country/state keys at the top level.
	if ( $none && ! empty( $country_state ) ) {
		$cities['none'] = esc_html__( 'None', 'gigneo' );
	}

	if ( ! empty( $term_query->terms ) ) {
		foreach ( $term_query->terms as $term ) {
			$country_state_meta = get_term_meta( $term->term_id, '_gigneo_tm_country_state', true );
			if ( ! empty( $country_state_meta ) ) {
				if ( ! empty( $country_state ) ) {
					$cities[ $term->slug ] = $term->name;
				} else {
					if ( ! isset( $cities[ $country_state_meta ] ) || ! is_array( $cities[ $country_state_meta ] ) ) {
						$cities[ $country_state_meta ] = array();
					}
					$cities[ $country_state_meta ][ $term->slug ] = $term->name;
				}
			}
		}
	}

	if ( ! empty( $country_state ) ) {
		return apply_filters( "gigneo_country_state_{$country_state}_cities", $cities );
	}

	return apply_filters( 'gigneo_country_state_cities', $cities );
}

/**
 * Get location label based on location value (country, state or city).
 *
 * @param string $location Location value.
 * @return string Location label.
 * @since 0.0.1
 */
function gigneo_get_location_label( $location ) {
	if ( empty( $location ) ) {
		return '';
	}

	$country_state_list = gigneo_countries_with_states( 'active' );
	if ( ! empty( $country_state_list[ $location ] ) ) {
		return $country_state_list[ $location ];
	}

	$allowed_countries = gigneo_allowed_countries();
	if ( ! empty( $allowed_countries[ $location ] ) ) {
		return $allowed_countries[ $location ];
	}

	$city_term = get_term_by( 'slug', $location, 'gigneo_city' );
	if ( $city_term && ! is_wp_error( $city_term ) ) {
		return $city_term->name;
	}

	return $location;
}

/**
 * Retrieve list of active Gigneo modules.
 *
 * @return array Active modules.
 * @since 0.0.1
 */
function gigneo_active_modules() {
	return Modules::active_modules();
}

/**
 * Get social profiles.
 *
 * This function returns an array of social profiles.
 *
 * @return array The array of social profiles.
 * @since 0.0.1
 */
function gigneo_social_profiles( $type = '' ) {
	$socials = array(
		'facebook'    => esc_html__( 'Facebook', 'gigneo' ),
		'twitter'     => esc_html__( 'Twitter', 'gigneo' ),
		'instagram'   => esc_html__( 'Instagram', 'gigneo' ),
		'linkedin'    => esc_html__( 'LinkedIn', 'gigneo' ),
		'youtube'     => esc_html__( 'YouTube', 'gigneo' ),
		'tiktok'      => esc_html__( 'TikTok', 'gigneo' ),
		'pinterest'   => esc_html__( 'Pinterest', 'gigneo' ),
		'snapchat'    => esc_html__( 'Snapchat', 'gigneo' ),
		'reddit'      => esc_html__( 'Reddit', 'gigneo' ),
		'tumblr'      => esc_html__( 'Tumblr', 'gigneo' ),
		'flickr'      => esc_html__( 'Flickr', 'gigneo' ),
		'twitch'      => esc_html__( 'Twitch', 'gigneo' ),
		'discord'     => esc_html__( 'Discord', 'gigneo' ),
		'whatsapp'    => esc_html__( 'WhatsApp', 'gigneo' ),
		'telegram'    => esc_html__( 'Telegram', 'gigneo' ),
		'wechat'      => esc_html__( 'WeChat', 'gigneo' ),
		'viber'       => esc_html__( 'Viber', 'gigneo' ),
		'vk'          => esc_html__( 'VK', 'gigneo' ),
		'mix'         => esc_html__( 'Mix', 'gigneo' ),
		'mastodon'    => esc_html__( 'Mastodon', 'gigneo' ),
		'signal'      => esc_html__( 'Signal', 'gigneo' ),
		'line'        => esc_html__( 'LINE', 'gigneo' ),
		'medium'      => esc_html__( 'Medium', 'gigneo' ),
		'clubhouse'   => esc_html__( 'Clubhouse', 'gigneo' ),
		'behance'     => esc_html__( 'Behance', 'gigneo' ),
		'dribbble'    => esc_html__( 'Dribbble', 'gigneo' ),
		'soundcloud'  => esc_html__( 'SoundCloud', 'gigneo' ),
		'spotify'     => esc_html__( 'Spotify', 'gigneo' ),
		'apple_music' => esc_html__( 'Apple Music', 'gigneo' ),
		'bandcamp'    => esc_html__( 'Bandcamp', 'gigneo' ),
		'patreon'     => esc_html__( 'Patreon', 'gigneo' ),
		'substack'    => esc_html__( 'Substack', 'gigneo' ),
		'rss'         => esc_html__( 'RSS Feed', 'gigneo' ),
		'github'      => esc_html__( 'Github', 'gigneo' ),
		'website'     => esc_html__( 'Website', 'gigneo' ),
		'custom'      => esc_html__( 'Custom', 'gigneo' ),
	);

	$socials = apply_filters( 'gigneo_social_profiles', $socials );

	if ( 'active' === $type ) {
		$active_keys = gigneo_option( '_gigneo_ts_freelancer_social_profiles' );

		if ( is_array( $active_keys ) ) {
			$socials = array_intersect_key( $socials, array_flip( $active_keys ) );
		}
	}

	return $socials;
}

/**
 * Get english language levels.
 *
 * This function returns an array of language levels.
 *
 * @return array The array of language levels.
 * @since 0.0.1
 */
function gigneo_language_levels( $none = true ) {

	$levels = array(
		'none'           => esc_html__( 'None', 'gigneo' ),
		'basic'          => esc_html__( 'Basic', 'gigneo' ),
		'conversational' => esc_html__( 'Conversational', 'gigneo' ),
		'fluent'         => esc_html__( 'Fluent', 'gigneo' ),
		'native'         => esc_html__( 'Native', 'gigneo' ),
	);

	if ( ! $none ) {
		unset( $levels['none'] );
	}

	return apply_filters( 'gigneo_language_levels', $levels, $none );
}

/**
 * Get genders.
 *
 * This function returns an array of genders.
 *
 * @return array The array of gender types.
 * @since 0.0.1
 */
function gigneo_genders( $none = true ) {

	$genders = array(
		'none'   => esc_html__( 'None', 'gigneo' ),
		'male'   => esc_html__( 'Male', 'gigneo' ),
		'female' => esc_html__( 'Female', 'gigneo' ),
		'other'  => esc_html__( 'Other', 'gigneo' ),
	);

	if ( ! $none ) {
		unset( $genders['none'] );
	}

	return apply_filters( 'gigneo_genders', $genders, $none );
}

/**
 * Get qualifications.
 *
 * This function returns an array of qualifications.
 *
 * @return array The array of qualification types.
 * @since 0.0.1
 */
function gigneo_qualifications( $none = true ) {

	$qualifications = array(
		'none'               => esc_html__( 'None', 'gigneo' ),
		'high_school'        => esc_html__( 'High School Graduate', 'gigneo' ),
		'bachelor'           => esc_html__( 'Bachelor’s Degree', 'gigneo' ),
		'master'             => esc_html__( 'Master’s Degree', 'gigneo' ),
		'doctorate'          => esc_html__( 'Doctorate (PhD)', 'gigneo' ),
		'professional_cert'  => esc_html__( 'Professional Certification', 'gigneo' ),
		'bootcamp'           => esc_html__( 'Bootcamp Graduate', 'gigneo' ),
		'currently_studying' => esc_html__( 'Currently Studying', 'gigneo' ),
	);

	if ( ! $none ) {
		unset( $qualifications['none'] );
	}

	return apply_filters( 'gigneo_qualifications', $qualifications, $none );
}

/**
 * Retrieve available profile types.
 *
 * Returns an array of profile types available in Gigneo.
 *
 * @since 0.0.1
 *
 * @return array Profile types keyed by slug.
 */
function gigneo_profile_types() {
	$types = array(
		'independent' => esc_html__( 'Independent', 'gigneo' ),
	);

	return apply_filters( 'gigneo_profile_types', $types );
}

/**
 * Get the user's name.
 *
 * This function retrieves the user's name based on the user ID.
 *
 * @param int $user_id The ID of the user.
 * @return string The user's name.
 * @since 0.0.1
 */
function gigneo_profile_name( $profile_id ) {
	$name         = '';
	$user_id      = gigneo_post_meta( $profile_id, '_gigneo_pm_user_id' );
	$profile_type = gigneo_profile_type( $profile_id );
	$first_name   = '';
	$last_name    = '';

	// Step 1: Try to get name from metadata for freelancer or employer profiles
	if ( 'freelancer' === $profile_type || 'employer' === $profile_type ) {
		$first_name = gigneo_post_meta( $profile_id, '_gigneo_pm_first_name' );
		$last_name  = gigneo_post_meta( $profile_id, '_gigneo_pm_last_name' ); // Fixed key
	}

	// Step 2: If metadata is available, construct name based on type
	$type = gigneo_option( '_gigneo_ts_display_user_name', 'full' );
	if ( ! empty( $first_name ) ) {
		if ( 'full' === $type ) {
			$name = ! empty( $last_name ) ? $first_name . ' ' . $last_name : $first_name;
		} elseif ( 'nick' === $type ) {
			$name = $first_name;
		} elseif ( 'short' === $type ) {
			$name = ! empty( $last_name ) ? $first_name . ' ' . strtoupper( substr( $last_name, 0, 1 ) ) : $first_name;
		} else {
			$name = $first_name;
		}
	}

	// Step 3: If no name from metadata, fall back to user data
	if ( empty( $name ) ) {
		if ( empty( $user_id ) ) {
			$user_id = get_current_user_id();
		}

		$user_data = get_userdata( $user_id );

		if ( ! empty( $user_data ) ) {
			if ( 'full' === $type ) {
				$name = ! empty( $user_data->last_name ) ? $user_data->first_name . ' ' . $user_data->last_name : $user_data->first_name;
			} elseif ( 'nick' === $type ) {
				$name = $user_data->nickname;
			} elseif ( 'short' === $type ) {
				$name = ! empty( $user_data->last_name ) ? $user_data->first_name . ' ' . strtoupper( substr( $user_data->last_name, 0, 1 ) ) : $user_data->first_name;
			} else {
				$name = $user_data->first_name;
			}

			// Final fallback to display_name
			if ( empty( $name ) ) {
				$name = $user_data->display_name;
			}
		}
	}

	return apply_filters( 'gigneo_profile_name', $name, $type, $user_id );
}

/**
 * Get the user's avatar.
 *
 * This function retrieves the user's avatar based on the user ID and type.
 *
 * @param int $user_id The ID of the user.
 * @param string $user_type The type of the user (freelancer or employer).
 * @param string $size The size of the avatar.
 * @return string The URL of the user's avatar.
 * @since 0.0.1
 */
function gigneo_profile_avatar_url( $profile_id = '', $profile_type = '', $size = 'medium' ) {
	$image_url = '';

	if ( empty( $profile_id ) ) {
		$profile_id = gigneo_active_profile_id();
	}

	if ( empty( $profile_type ) ) {
		$profile_type = gigneo_profile_types();
	}

	$user_id = gigneo_post_meta( $profile_id, '_gigneo_pm_user_id' );

	if ( 'freelancer' === $profile_type || 'employer' === $profile_type ) {

		$image_url = get_the_post_thumbnail_url( $profile_id, $size );

		if ( empty( $image_url ) ) {
			$default_option_key = 'freelancer' === $profile_type ? '_gigneo_ts_freelancer_default_profile_image' : '_gigneo_ts_employer_default_profile_image';

			$default_image = gigneo_option( $default_option_key );
			$image_url     = ! empty( $default_image ) ? wp_get_attachment_image_url( $default_image, $size ) : '';
		}
	}

	if ( empty( $image_url ) ) {
		$image_url = get_avatar_url( $user_id );
	}

	if ( empty( $image_url ) ) {
		$image_url = gigneo_placeholder_url();
	}

	return apply_filters( 'gigneo_profile_avatar_url', $image_url, $profile_id, $profile_type, $size );
}

/**
 * Get the moderator display name.
 *
 * Returns the configured moderator name or the default localized string.
 *
 * @since 0.0.1
 *
 * @return string Moderator name.
 */
function gigneo_moderator_name() {
	$name = gigneo_option( '_gigneo_ts_moderator_name', esc_html__( 'Moderator', 'gigneo' ) );
	return apply_filters( 'gigneo_moderator_name', $name );
}

/**
 * Get the moderator label.
 *
 * Returns the configured moderator label or the default localized string.
 *
 * @since 0.0.1
 *
 * @return string Moderator label.
 */
function gigneo_moderator_label() {
	$label = gigneo_option( '_gigneo_ts_moderator_label', esc_html__( 'Support Team', 'gigneo' ) );
	return apply_filters( 'gigneo_moderator_label', $label );
}

/**
 * Get employer profile access level.
 *
 * Returns the configured access level for employer profiles.
 *
 * @since 0.0.1
 *
 * @return string Access level (e.g., 'public', 'private').
 */
function gigneo_employer_profile_access() {
	$access = gigneo_option( '_gigneo_ts_employer_profile_access', 'public' );

	return apply_filters( 'gigneo_employer_profile_access', $access );
}

/**
 * Get moderator avatar attachment ID.
 *
 * Returns the configured avatar attachment ID for the moderator.
 *
 * @since 0.0.1
 *
 * @return int Attachment ID (0 if not set).
 */
function gigneo_moderator_avatar_id() {
	$avatar_id = gigneo_option( '_gigneo_ts_moderator_avatar', 0 );
	return apply_filters( 'gigneo_moderator_avatar_id', $avatar_id );
}

/**
 * Retrieve the list of Google Fonts.
 *
 * This function returns an array of Google Fonts available for use.
 *
 * @return array The array of Google Fonts.
 * @since 0.0.1
 */
function gigneo_google_font_list() {
	$fonts = array(
		''                 => esc_html__( 'Default', 'gigneo' ),
		'Inter'            => esc_html__( 'Inter', 'gigneo' ),
		'Roboto'           => esc_html__( 'Roboto', 'gigneo' ),
		'DM Sans'          => esc_html__( 'DM Sans', 'gigneo' ),
		'Lato'             => esc_html__( 'Lato', 'gigneo' ),
		'Montserrat'       => esc_html__( 'Montserrat', 'gigneo' ),
		'Poppins'          => esc_html__( 'Poppins', 'gigneo' ),
		'Raleway'          => esc_html__( 'Raleway', 'gigneo' ),
		'Open Sans'        => esc_html__( 'Open Sans', 'gigneo' ),
		'Golos Text'       => esc_html__( 'Golos Text', 'gigneo' ),
		'Figtree'          => esc_html__( 'Figtree', 'gigneo' ),
		'Merriweather'     => esc_html__( 'Merriweather', 'gigneo' ),
		'Oswald'           => esc_html__( 'Oswald', 'gigneo' ),
		'Gilroy'           => esc_html__( 'Gilroy', 'gigneo' ),
		'Playfair Display' => esc_html__( 'Playfair Display', 'gigneo' ),
		'Ubuntu'           => esc_html__( 'Ubuntu', 'gigneo' ),
		'Nunito'           => esc_html__( 'Nunito', 'gigneo' ),
		'Rubik'            => esc_html__( 'Rubik', 'gigneo' ),
		'Source Sans Pro'  => esc_html__( 'Source Sans Pro', 'gigneo' ),
		'Work Sans'        => esc_html__( 'Work Sans', 'gigneo' ),
		'PT Sans'          => esc_html__( 'PT Sans', 'gigneo' ),
		'Bebas Neue'       => esc_html__( 'Bebas Neue', 'gigneo' ),
		'Titillium Web'    => esc_html__( 'Titillium Web', 'gigneo' ),
		'Dancing Script'   => esc_html__( 'Dancing Script', 'gigneo' ),
		'Josefin Sans'     => esc_html__( 'Josefin Sans', 'gigneo' ),
		'Quicksand'        => esc_html__( 'Quicksand', 'gigneo' ),
		'Pacifico'         => esc_html__( 'Pacifico', 'gigneo' ),
		'Caveat'           => esc_html__( 'Caveat', 'gigneo' ),
		'Fira Sans'        => esc_html__( 'Fira Sans', 'gigneo' ),
		'Inconsolata'      => esc_html__( 'Inconsolata', 'gigneo' ),
		'Anton'            => esc_html__( 'Anton', 'gigneo' ),
		'Arimo'            => esc_html__( 'Arimo', 'gigneo' ),
		'Barlow'           => esc_html__( 'Barlow', 'gigneo' ),
		'Cabin'            => esc_html__( 'Cabin', 'gigneo' ),
		'Cairo'            => esc_html__( 'Cairo', 'gigneo' ),
		'Kanit'            => esc_html__( 'Kanit', 'gigneo' ),
		'Muli'             => esc_html__( 'Muli', 'gigneo' ),
		'Overpass'         => esc_html__( 'Overpass', 'gigneo' ),
		'Teko'             => esc_html__( 'Teko', 'gigneo' ),
		'Geist'            => esc_html__( 'Geist', 'gigneo' ),
		'Varela Round'     => esc_html__( 'Varela Round', 'gigneo' ),
	);

	return apply_filters( 'gigneo_google_fonts_list', $fonts );
}

/**
 * Check if the Gigneo Pro version is active.
 *
 * This function checks if the Gigneo Pro version is active.
 *
 * @return bool True if the Gigneo Pro version is active, false otherwise.
 * @since 0.0.1
 */
function gigneo_pro_is_active() {
	return did_action( 'gigneo_pro_loaded' );
}

/**
 * Retrieve password strength options.
 *
 * @return array The array of password strength options.
 * @since 0.0.1
 */
function gigneo_password_strength_options() {
	$strength_options = array(
		'length'    => esc_html__( 'At least 8 characters', 'gigneo' ),
		'uppercase' => esc_html__( 'At least 1 uppercase', 'gigneo' ),
		'lowercase' => esc_html__( 'At lease 1 lowercase', 'gigneo' ),
		'number'    => esc_html__( 'At lease 1 number', 'gigneo' ),
		'special'   => esc_html__( 'At lease 1 special character', 'gigneo' ),
	);

	return apply_filters( 'gigneo_password_strength_options', $strength_options );
}

/**
 * Create a profile for a user.
 *
 * This function creates a profile post for a user based on their role.
 *
 * @param int $user_id The ID of the user.
 * @param string $role The role of the user ('freelancer' or 'employer').
 * @return int|false The ID of the profile post if created, otherwise false.
 * @since 0.0.1
 */
function gigneo_create_profile( $user_id, $role ) {
	if ( empty( $user_id ) ) {
		return false;
	}

	$user = get_userdata( $user_id );
	if ( ! $user ) {
		return false;
	}

	// Determine role if not provided
	if ( empty( $role ) || ! in_array( $role, array( 'freelancer', 'employer' ), true ) ) {
		$role = gigneo_option( '_gigneo_ts_default_user_role', 'employer' );
	}

	// Role to capability mapping
	$role_capabilities = array(
		'freelancer' => 'gigneo_freelancer',
		'employer'   => 'gigneo_employer',
	);

	// Ensure user has the required role or capability
	if ( ! isset( $role_capabilities[ $role ] ) || ! user_can( $user_id, $role_capabilities[ $role ] ) ) {
		return false;
	}

	// Check if a profile already exists
	$existing_profile = get_posts(
		array(
			'post_type'   => $role_capabilities[ $role ],
			'author'      => $user_id,
			'post_status' => 'publish',
			'numberposts' => 1,
			'fields'      => 'ids',
		)
	);

	if ( ! empty( $existing_profile ) ) {
		return false;
	}

	// Generate post title and slug
	$first_name = get_user_meta( $user_id, 'first_name', true );
	$last_name  = get_user_meta( $user_id, 'last_name', true );
	$username   = $user->user_login;

	$post_title = trim( "$first_name $last_name" );
	if ( empty( $post_title ) ) {
		$post_title = $username;
	}
	$post_slug = sanitize_title( $username );

	// Create profile post
	$profile_id = wp_insert_post(
		array(
			'post_title'  => $post_title,
			'post_name'   => $post_slug,
			'post_type'   => $role_capabilities[ $role ],
			'post_status' => 'publish',
			'post_author' => $user_id,
		)
	);

	if ( is_wp_error( $profile_id ) ) {
		return false;
	}

	// Set post-meta for profile
	update_post_meta( $profile_id, '_gigneo_pm_first_name', $first_name );
	update_post_meta( $profile_id, '_gigneo_pm_last_name', $last_name );
	update_post_meta( $profile_id, '_gigneo_pm_user_id', $user_id );
	update_post_meta( $profile_id, '_gigneo_pm_orders_completed', 0 );
	update_post_meta( $profile_id, '_gigneo_pm_average_rating', 0 );

	// Store profile reference in metadata
	update_user_meta( $user_id, '_gigneo_um_default_profile', $role );
	update_user_meta( $user_id, "_gigneo_um_{$role}_profile_id", $profile_id );

    // phpcs:ignore WordPress.WP.Capabilities.Unknown -- Registered capabilities
	if ( user_can( $user_id, 'gigneo_freelancer' ) && user_can( $user_id, 'gigneo_employer' ) ) {
		$default_profile = gigneo_option( '_gigneo_ts_default_user_role', 'employer' );
		update_user_meta( $user_id, '_gigneo_um_active_profile', $default_profile );
	} else {
		update_user_meta( $user_id, '_gigneo_um_active_profile', $role );
	}

	//Trigger Notification
	$notification_title = sprintf(
		// translators: 1: Site name.
		esc_html__( 'Welcome to %s', 'gigneo' ),
		get_bloginfo( 'name' )
	);

	$notification_content = sprintf(
		// translators: 1: Profile name.
		esc_html__( 'Hi %s, Your profile has been created successfully.', 'gigneo' ),
		gigneo_profile_name( $profile_id )
	);

	Notifications::trigger_notification( $notification_title, $notification_content, 'account', 'register_account', $profile_id );

	// Trigger action for custom hooks
	do_action( 'gigneo_after_profile_create', $user_id, $profile_id, $role );

	return (int) $profile_id;
}

/**
 * Get the profile ID for a user.
 *
 * This function retrieves the profile ID for a user based on their role.
 *
 * @param int $user_id The ID of the user.
 * @param string $role The role of the user ('freelancer' or 'employer').
 * @return int|false The ID of the profile post if found, otherwise false.
 * @since 0.0.1
 */
function gigneo_profile_id( $user_id, $role = '' ) {

	if ( empty( $user_id ) ) {
		return false;
	}

	$user = get_userdata( $user_id );

	if ( ! $user ) {
		return false;
	}

	if ( empty( $role ) ) {
		$role = get_user_meta( $user_id, '_gigneo_um_active_profile', true );
	}

	$role_capabilities = array(
		'freelancer' => 'gigneo_freelancer',
		'employer'   => 'gigneo_employer',
	);

    // phpcs:ignore WordPress.WP.Capabilities.Unknown -- Registered capabilities
	if ( ! isset( $role_capabilities[ $role ] ) && ! user_can( $user_id, 'gigneo_freelancer' ) && ! user_can( $user_id, 'gigneo_employer' ) ) {
		return false;
	}

	// Check for existing profile ID in user meta
	$profile_id   = get_user_meta( $user_id, "_gigneo_um_{$role}_profile_id", true );
	$profile_post = get_post( $profile_id );

	if ( ! empty( $profile_id ) && $profile_post ) {
		return (int) $profile_id;
	}

	return false;
}

/**
 * Get the active profile ID for a user.
 *
 * This function retrieves the active profile ID for a user based on their role.
 *
 * @param int $user_id The ID of the user.
 * @return int|false The ID of the active profile post if found, otherwise false.
 * @since 0.0.1
 */
function gigneo_active_profile_id( $user_id = '' ) {

	global $current_user;

	if ( empty( $user_id ) && isset( $current_user->ID ) ) {
		$user_id = get_current_user_id();
	}

	if ( empty( $user_id ) ) {
		return false;
	}

	$user = get_userdata( $user_id );

	if ( ! $user ) {
		return false;
	}

	$default_profile = gigneo_option( '_gigneo_ts_default_user_role', 'employer' );

	// Check if the role-switching module is active
	if ( in_array( 'switch_role', gigneo_active_modules(), true ) ) {
		$active_profile = get_user_meta( $user_id, '_gigneo_um_active_profile', true );
		$active_profile = empty( $active_profile ) ? $default_profile : $active_profile;
		return gigneo_profile_id( $user_id, $active_profile );
	}

	$role = '';

    // phpcs:disable WordPress.WP.Capabilities.Unknown
	if ( user_can( $user_id, 'gigneo_freelancer' ) && user_can( $user_id, 'gigneo_employer' ) ) {
		$role = $default_profile;
	} elseif ( user_can( $user_id, 'gigneo_freelancer' ) ) {
		$role = 'freelancer';
	} elseif ( user_can( $user_id, 'gigneo_employer' ) ) {
		$role = 'employer';
	} // phpcs:enable WordPress.WP.Capabilities.Unknown

	if ( ! empty( $role ) ) {
		return gigneo_profile_id( $user_id, $role );
	}

	return false;
}

/**
 * Get the profile type for a given profile ID.
 *
 * @param int $profile_id The ID of the profile.
 * @return string The profile type.
 * @since 0.0.1
 */
function gigneo_profile_type( $profile_id = '' ) {

	if ( empty( $profile_id ) ) {
		$profile_id = gigneo_active_profile_id();
	}

	if ( ! $profile_id ) {
		$current_user = wp_get_current_user();
		if ( ! empty( $current_user->roles ) ) {
			return $current_user->roles[0];
		}
		return false;
	}

	$profile_type = get_post_type( $profile_id );

	$role_capabilities = array(
		'gigneo_freelancer' => 'freelancer',
		'gigneo_employer'   => 'employer',
	);

	$type = $role_capabilities[ $profile_type ] ?? '';

	return apply_filters( 'gigneo_profile_type', $type, $profile_id );
}

/**
 * Get the online status of a profile.
 *
 * This function checks if a profile is currently online based on the last activity timestamp.
 *
 * @param int $profile_id The ID of the profile.
 * @return bool True if the profile is online, false otherwise.
 * @since 0.0.1
 */
function gigneo_online_status( $profile_id = '' ) {
	if ( empty( $profile_id ) ) {
		$profile_id = gigneo_active_profile_id();
	}

	$last_activity = gigneo_post_meta( $profile_id, '_gigneo_pm_last_activity' );
	$offline_until = gigneo_post_meta( $profile_id, '_gigneo_pm_offline_until' );
	$now           = time();

	// Check status
	if ( 'forever' === $offline_until ) {
		$status = false;
	} elseif ( is_numeric( $offline_until ) && $now < intval( $offline_until ) ) {
		$status = false;
	} elseif ( $last_activity && $last_activity >= ( $now - 300 ) ) {
		$status = true;
	} else {
		$status = false;
	}

	return apply_filters( 'gigneo_online_status', $status, $profile_id, $last_activity );
}

/**
 * Get online profile IDs by profile type.
 *
 * Optimized query that applies online status conditions in SQL.
 *
 * @param string $profile_type Required. Profile type: freelancer or employer.
 *
 * @return array Flat array of online profile IDs.
 * @since 0.0.1
 */
function gigneo_online_profiles( $profile_type = '' ) {
	global $wpdb;

	$profile_type = sanitize_key( $profile_type );

	$type_map = array(
		'freelancer' => 'gigneo_freelancer',
		'employer'   => 'gigneo_employer',
	);

	if ( empty( $profile_type ) || ! isset( $type_map[ $profile_type ] ) ) {
		return apply_filters( 'gigneo_online_profiles', array(), $profile_type );
	}

	$post_type      = $type_map[ $profile_type ];
	$activity_after = time() - 300;
	$now            = time();
	$posts_table    = $wpdb->posts;
	$postmeta_table = $wpdb->postmeta;

	// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	$online_ids = $wpdb->get_col(
		$wpdb->prepare(
			"
			SELECT DISTINCT p.ID
			FROM %i p
			INNER JOIN %i pm_last
				ON pm_last.post_id = p.ID
				AND pm_last.meta_key = '_gigneo_pm_last_activity'
			LEFT JOIN %i pm_offline
				ON pm_offline.post_id = p.ID
				AND pm_offline.meta_key = '_gigneo_pm_offline_until'
			WHERE p.post_type = %s
				AND p.post_status = 'publish'
				AND pm_last.meta_value REGEXP '^[0-9]+\$'
				AND CAST(pm_last.meta_value AS UNSIGNED) >= %d
				AND (
					pm_offline.post_id IS NULL
					OR pm_offline.meta_value = ''
					OR (
						pm_offline.meta_value != 'forever'
						AND (
							pm_offline.meta_value NOT REGEXP '^[0-9]+\$'
							OR CAST(pm_offline.meta_value AS UNSIGNED) <= %d
						)
					)
				)
			",
			$posts_table,
			$postmeta_table,
			$postmeta_table,
			$post_type,
			$activity_after,
			$now
		)
	);
	// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	$online_ids = wp_parse_id_list( $online_ids );

	return apply_filters( 'gigneo_online_profiles', $online_ids, $profile_type );
}

/**
 * Get verified profile IDs by profile type.
 *
 * Uses the same verification conditions as profile verification helpers.
 *
 * @param string $profile_type Required. Profile type: freelancer or employer.
 * @return array Flat array of verified profile IDs.
 * @since 0.0.1
 */
function gigneo_verified_profiles( $profile_type = '' ) {
	global $wpdb;

	$profile_type = sanitize_key( $profile_type );

	$type_map = array(
		'freelancer' => 'gigneo_freelancer',
		'employer'   => 'gigneo_employer',
	);

	if ( empty( $profile_type ) || ! isset( $type_map[ $profile_type ] ) ) {
		return array();
	}

	$posts_table    = $wpdb->posts;
	$usermeta_table = $wpdb->usermeta;
	$postmeta_table = $wpdb->postmeta;

	$profile_post_type = $type_map[ $profile_type ];
	$profile_key       = "_gigneo_um_{$profile_type}_profile_id";

	$with_identity = apply_filters( 'gigneo_verified_profiles_with_identity', false, $profile_type );

	if ( $with_identity ) {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$verified_profile_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT DISTINCT p.ID
				FROM %i p
				INNER JOIN %i um_profile
					ON um_profile.meta_value = p.ID
					AND um_profile.meta_key = %s
				INNER JOIN %i um_email
					ON um_email.user_id = um_profile.user_id
					AND um_email.meta_key = '_gigneo_um_email_verified'
					AND um_email.meta_value = '1'
				INNER JOIN %i pm_identity ON pm_identity.post_id = p.ID
					AND pm_identity.meta_key = '_gigneo_pm_identity_verification'
					AND pm_identity.meta_value = 'verified'
				WHERE p.post_type = %s AND p.post_status = 'publish'",
				$posts_table,
				$usermeta_table,
				$profile_key,
				$usermeta_table,
				$postmeta_table,
				$profile_post_type
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	} else {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$verified_profile_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT DISTINCT p.ID
				FROM %i p
				INNER JOIN %i um_profile
					ON um_profile.meta_value = p.ID
					AND um_profile.meta_key = %s
				INNER JOIN %i um_email
					ON um_email.user_id = um_profile.user_id
					AND um_email.meta_key = '_gigneo_um_email_verified'
					AND um_email.meta_value = '1'
				WHERE p.post_type = %s AND p.post_status = 'publish'",
				$posts_table,
				$usermeta_table,
				$profile_key,
				$usermeta_table,
				$profile_post_type
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}
	$verified_profile_ids = wp_parse_id_list( $verified_profile_ids );

	return apply_filters( 'gigneo_verified_profiles', $verified_profile_ids, $profile_type );
}

/**
 * Check if a user has access to the dashboard.
 *
 * This function checks if a user has access to the dashboard based on their role or capabilities.
 *
 * @param int $user_id The ID of the user.
 * @return bool True if the user has access, false otherwise.
 * @since 0.0.1
 */
function gigneo_user_have_access( $user_id = 0 ) {
	$access = false;

	if ( empty( $user_id ) ) {
		$user_id = get_current_user_id();
	}

	if ( ! $user_id ) {
		return false;
	}

	$user = get_userdata( $user_id );

	if ( ! $user ) {
		return false;
	}

	$allowed_roles = array( 'gigneo_employer', 'gigneo_freelancer' );

	foreach ( $allowed_roles as $role ) {

		$has_cap = $user->has_cap( $role );

		if ( $has_cap ) {
			$profile_key  = ( 'gigneo_employer' === $role ) ? '_gigneo_um_employer_profile_id' : '_gigneo_um_freelancer_profile_id';
			$profile_id   = (int) get_user_meta( $user_id, $profile_key, true );
			$profile_post = get_post( $profile_id );
			if ( $profile_id && $profile_post ) {
				$access = true;
				break;
			}
		}
	}

	return (bool) apply_filters( 'gigneo_user_have_access', $access, $user_id );
}

/**
 * Retrieve the list of user menus.
 *
 * This function returns an array of user menus.
 *
 * @return array The array of user menus.
 * @since 0.0.1
 */
function gigneo_user_menu_list( $user_type = '' ) {

	$menus = array(
		'home'      => array(
			'title'    => esc_html__( 'Home', 'gigneo' ),
			'icon'     => 'hi hi-home',
			'url'      => site_url( '/' ),
			'source'   => '',
			'ref'      => '',
			'roles'    => array( 'administrator' ),
			'target'   => '_self',
			'class'    => '',
			'priority' => 1,
		),
		'dashboard' => array(
			'title'    => esc_html__( 'Dashboard', 'gigneo' ),
			'icon'     => 'hi hi-home',
			'url'      => '',
			'source'   => '',
			'ref'      => '',
			'roles'    => array( 'employer', 'freelancer' ),
			'target'   => '_self',
			'class'    => '',
			'priority' => 1,
		),
		'profile'   => array(
			'title'    => esc_html__( 'My Profile', 'gigneo' ),
			'icon'     => 'hi hi-user',
			'url'      => '',
			'source'   => 'setting',
			'ref'      => 'profile',
			'roles'    => array( 'employer', 'freelancer' ),
			'target'   => '_self',
			'id'       => '',
			'class'    => '',
			'priority' => 5,
		),
		'wallet'    => array(
			'title'    => esc_html__( 'Wallet', 'gigneo' ),
			'icon'     => 'hi hi-wallet',
			'url'      => '',
			'source'   => 'account',
			'ref'      => 'wallet',
			'roles'    => array( 'freelancer', 'employer' ),
			'target'   => '_self',
			'id'       => '',
			'class'    => '',
			'priority' => 10,
		),
		'setting'   => array(
			'title'    => esc_html__( 'Settings', 'gigneo' ),
			'icon'     => 'hi hi-cog-8-tooth',
			'url'      => '',
			'source'   => 'setting',
			'ref'      => 'account',
			'roles'    => array( 'freelancer', 'employer' ),
			'target'   => '_self',
			'id'       => '',
			'class'    => '',
			'priority' => 15,
		),
	);

	$menus = apply_filters( 'gigneo_user_menu_list', $menus, $user_type );

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

	// Filter menus based on user type
	$menus = array_filter(
		$menus,
		function ( $menu ) use ( $user_type ) {
			return in_array( $user_type, $menu['roles'], true );
		}
	);

	// Sort by priority
	uasort(
		$menus,
		function ( $a, $b ) {
			return $a['priority'] <=> $b['priority'];
		}
	);

	return $menus;
}

/**
 * Retrieve the list of all dashboard menus.
 *
 * This function returns an array of all available dashboard menus.
 *
 * @return array The array of dashboard menus.
 * @since 0.0.1
 */
function gigneo_dashboard_menu_list( $user_type = '', $group = '' ) {

	$post_types_slug = Post_Types::post_type_slug();
	$platform_type   = gigneo_platform_type();
	$profile_access  = gigneo_employer_profile_access();
	$profile_id      = gigneo_active_profile_id();
	$profile_type    = gigneo_profile_type( $profile_id );

	$unread_notifications = Notifications::get_unread_notification_count();
	$unread_notifications = ! empty( $unread_notifications ) ? $unread_notifications : '';

	$active_order_count = gigneo_profile_stat( 'active_orders_count', $profile_id );
	$active_order_count = ! empty( $active_order_count ) ? $active_order_count['lifetime'] : '';

	$menus = array(
		'dashboard'    => array(
			'title'    => esc_html__( 'Dashboard', 'gigneo' ),
			'icon'     => 'hi hi-home',
			'url'      => '',
			'source'   => 'account',
			'ref'      => 'overview',
			'roles'    => array( 'employer', 'freelancer' ),
			'group'    => 'platform',
			'submenu'  => apply_filters( 'gigneo_dashboard_submenu_list', array() ),
			'target'   => '_self',
			'id'       => '',
			'class'    => '',
			'priority' => 1,
		),
		'service'      => array(
			'title'    => esc_html__( 'Services', 'gigneo' ),
			'icon'     => 'hi hi-archive-box',
			'url'      => '',
			'source'   => $post_types_slug['gigneo_service'],
			'ref'      => 'list',
			'roles'    => array( 'freelancer' ),
			'group'    => 'platform',
			'submenu'  => apply_filters( 'gigneo_service_submenu_list', array() ),
			'target'   => '_self',
			'id'       => '',
			'class'    => '',
			'priority' => 10,
		),
		'job'          => array(
			'title'    => esc_html__( 'Jobs', 'gigneo' ),
			'icon'     => 'hi hi-rectangle-stack',
			'url'      => '',
			'source'   => $post_types_slug['gigneo_job'],
			'ref'      => 'list',
			'roles'    => array( 'employer' ),
			'group'    => 'platform',
			'submenu'  => apply_filters( 'gigneo_job_submenu_list', array() ),
			'target'   => '_self',
			'id'       => '',
			'class'    => '',
			'priority' => 15,
		),
		'proposal'     => array(
			'title'    => esc_html__( 'Proposals', 'gigneo' ),
			'icon'     => 'hi hi-document-text',
			'url'      => '',
			'source'   => 'proposal',
			'ref'      => 'list',
			'roles'    => array( 'freelancer' ),
			'group'    => 'platform',
			'submenu'  => apply_filters( 'gigneo_proposal_submenu_list', array() ),
			'target'   => '_self',
			'id'       => '',
			'class'    => '',
			'priority' => 20,
		),
		'order'        => array(
			'title'    => esc_html__( 'Orders', 'gigneo' ),
			'icon'     => 'hi hi-briefcase',
			'url'      => '',
			'source'   => 'order',
			'count'    => $active_order_count > 0 ? $active_order_count : '',
			'ref'      => ( 'both' !== $platform_type ? ( 'services' === $platform_type ? 'service' : 'job' ) : 'service' ) . '-list',
			'roles'    => array( 'freelancer', 'employer' ),
			'group'    => 'platform',
			'submenu'  => apply_filters( 'gigneo_order_submenu_list', array() ),
			'target'   => '_self',
			'id'       => '',
			'class'    => '',
			'priority' => 25,
		),
		'saved'        => array(
			'title'    => esc_html__( 'Saved', 'gigneo' ),
			'icon'     => 'hi hi-heart',
			'url'      => '',
			'source'   => 'saved',
			'ref'      => 'list',
			'roles'    => array( 'freelancer', 'employer' ),
			'group'    => 'platform',
			'submenu'  => apply_filters( 'gigneo_favorite_submenu_list', array() ),
			'target'   => '_self',
			'id'       => '',
			'class'    => '',
			'priority' => 30,
		),
		'notification' => array(
			'title'    => esc_html__( 'Notifications', 'gigneo' ),
			'icon'     => 'hi hi-bell-alert',
			'url'      => '',
			'source'   => 'notification',
			'ref'      => 'all',
			'count'    => $unread_notifications,
			'roles'    => array( 'freelancer', 'employer' ),
			'group'    => 'account',
			'submenu'  => apply_filters( 'gigneo_notification_submenu_list', array() ),
			'target'   => '_self',
			'id'       => '',
			'class'    => '',
			'priority' => 35,
		),
		'setting'      => array(
			'title'    => esc_html__( 'Settings', 'gigneo' ),
			'icon'     => 'hi hi-cog-8-tooth',
			'url'      => '',
			'source'   => 'setting',
			'ref'      => 'account',
			'roles'    => array( 'freelancer', 'employer' ),
			'group'    => 'account',
			'submenu'  => apply_filters( 'gigneo_dispute_submenu_list', array() ),
			'target'   => '_self',
			'id'       => '',
			'class'    => '',
			'priority' => 40,
		),
	);

	$platform_type = gigneo_platform_type();
	$menus         = apply_filters( 'gigneo_dashboard_menu_list', $menus );

	if ( 'services' === $platform_type ) {
		unset( $menus['job'] );
		unset( $menus['proposal'] );
	}

	if ( 'jobs' === $platform_type ) {
		unset( $menus['service'] );
	}

	if ( 'freelancer' === $profile_type && 'private' === $profile_access && 'services' === $platform_type ) {
		unset( $menus['saved'] );
	}

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

	$menus = array_filter(
		$menus,
		function ( $menu ) use ( $user_type ) {
			return in_array( $user_type, $menu['roles'], true );
		}
	);

	// Filter by group
	if ( ! empty( $group ) ) {
		$menus = array_filter(
			$menus,
			function ( $menu ) use ( $group ) {
				return isset( $menu['group'] ) && $menu['group'] === $group;
			}
		);
	}

	// Sort main menu by priority
	uasort(
		$menus,
		function ( $a, $b ) {
			return $a['priority'] <=> $b['priority'];
		}
	);

	// Sort submenu by priority (if exists)
	foreach ( $menus as &$menu ) {
		if ( ! empty( $menu['submenu'] ) && is_array( $menu['submenu'] ) ) {
			uasort(
				$menu['submenu'],
				function ( $a, $b ) {
					return $a['priority'] <=> $b['priority'];
				}
			);
		}
	}
	unset( $menu );

	return $menus;
}

/**
 * Retrieve the URL for a dashboard menu item.
 *
 * This function constructs the URL for a specific dashboard menu item based on the provided source and reference.
 *
 * @param string $source The source identifier for the menu item.
 * @param string $ref The reference identifier for the menu item.
 * @param array $params Optional. Additional query parameters.
 * @return string The constructed URL for the dashboard menu item.
 * @since 0.0.1
 * @return string The generated dashboard menu URL.
 */
function gigneo_dashboard_url( $source = '', $ref = '', $params = array() ) {
	// Retrieve the dashboard page ID and its permalink.
	$page_id  = gigneo_option( '_gigneo_ts_template_dashboard' );
	$menu_url = ! empty( $page_id ) ? get_permalink( $page_id ) : home_url();

	$service_slug = Post_Types::post_type_slug( 'gigneo_service' );
	$job_slug     = Post_Types::post_type_slug( 'gigneo_job' );

	if ( 'service' !== $service_slug ) {
		$source = str_replace( 'service', Post_Types::post_type_slug( 'gigneo_service' ), $source );
		$ref    = str_replace( 'service', Post_Types::post_type_slug( 'gigneo_service' ), $ref );
	}

	if ( 'job' !== $job_slug ) {
		$source = str_replace( 'job', Post_Types::post_type_slug( 'gigneo_job' ), $source );
		$ref    = str_replace( 'job', Post_Types::post_type_slug( 'gigneo_job' ), $ref );
	}

	// Add query parameters if provided.
	$query_args = array();

	if ( ! empty( $source ) ) {
		$query_args['source'] = sanitize_text_field( $source );
	}
	if ( ! empty( $ref ) ) {
		$query_args['ref'] = sanitize_text_field( $ref );
	}

	if ( ! empty( $params ) && is_array( $params ) ) {
		foreach ( $params as $key => $value ) {
			$query_args[ sanitize_key( $key ) ] = sanitize_text_field( $value );
		}
	}

	// Append query arguments to the menu URL.
	if ( ! empty( $query_args ) ) {
		$menu_url = add_query_arg( $query_args, $menu_url );
	}

	// Allow developers to filter the menu URL before returning.
	return apply_filters( 'gigneo_dashboard_url', $menu_url, $source, $ref, $params );
}

/**
 * Retrieves the list of allowed file extensions.
 *
 * This function returns an array of permitted file extensions categorized into different types,
 * such as images, documents, compressed files, audio, video, and others. Filters are applied to
 * allow customization of supported file extensions.
 *
 * @param array $types Optional. An array of specific file types to retrieve (e.g., 'image', 'documents').
 * @return array The filtered array of allowed file extensions.
 * @since 0.0.1
 */
function gigneo_file_extensions( $types = array() ) {

	$file_categories = array(
		'image'      => apply_filters(
			'gigneo_image_extensions',
			array(
				'png'  => 'PNG',
				'jpeg' => 'JPEG',
				'jpg'  => 'JPG',
				'gif'  => 'GIF',
				'bmp'  => 'BMP',
				'webp' => 'WEBP',
				'svg'  => 'SVG',
			)
		),
		'document'   => apply_filters(
			'gigneo_document_extensions',
			array(
				'pdf'  => 'PDF',
				'doc'  => 'DOC',
				'docx' => 'DOCX',
				'txt'  => 'TXT',
				'rtf'  => 'RTF',
				'xls'  => 'XLS',
				'xlsx' => 'XLSX',
				'csv'  => 'CSV',
				'ppt'  => 'PPT',
				'pptx' => 'PPTX',
			)
		),
		'compressed' => apply_filters(
			'gigneo_compressed_extensions',
			array(
				'zip' => 'ZIP',
				'rar' => 'RAR',
				'7z'  => '7Z',
				'tar' => 'TAR',
				'gz'  => 'GZ',
			)
		),
		'audio'      => apply_filters(
			'gigneo_audio_extensions',
			array(
				'mp3'  => 'MP3',
				'wav'  => 'WAV',
				'ogg'  => 'OGG',
				'flac' => 'FLAC',
				'aac'  => 'AAC',
			)
		),
		'video'      => apply_filters(
			'gigneo_video_extensions',
			array(
				'mp4' => 'MP4',
				'avi' => 'AVI',
				'mkv' => 'MKV',
				'mov' => 'MOV',
			)
		),
		'other'      => apply_filters(
			'gigneo_other_extensions',
			array(
				'json' => 'JSON',
				'xml'  => 'XML',
				'html' => 'HTML',
				'css'  => 'CSS',
				'js'   => 'JS',
			)
		),
	);

	$flat_extensions = array();

	if ( ! empty( $types ) ) {
		foreach ( $types as $type ) {
			if ( isset( $file_categories[ $type ] ) ) {
				$flat_extensions = array_merge( $flat_extensions, $file_categories[ $type ] );
			}
		}
	} else {
		foreach ( $file_categories as $category ) {
			$flat_extensions = array_merge( $flat_extensions, $category );
		}
	}

	return apply_filters( 'gigneo_file_extensions', $flat_extensions );
}

/**
 * Retrieve a list of WooCommerce products.
 *
 * @param array $args Optional. An array of query arguments to retrieve products. Default is an empty array.
 * @return array The array of products.
 * @since 0.0.1
 */
function gigneo_product_list( $args = array() ) {

	$default_args = array(
		'post_type'      => 'product',
		'posts_per_page' => -1,
		'post_status'    => 'publish',
	);

	// Apply filter on query arguments
	$query_args = apply_filters( 'gigneo_product_list_args', wp_parse_args( $args, $default_args ) );

	$products     = get_posts( $query_args );
	$product_list = array();

	$product_list[''] = esc_html__( 'None', 'gigneo' );

	foreach ( $products as $product ) {
		$product_list[ $product->ID ] = get_the_title( $product->ID );
	}

	// Apply filter on the product list
	return apply_filters( 'gigneo_product_list', $product_list, $query_args );
}

/**
 * Retrieve the list of available withdrawal payout options.
 *
 * @return array The array of payout options.
 * @since 0.0.1
 */
function gigneo_withdrawal_payout_list() {

	$payout_list = array(
		'paypal'   => esc_html__( 'Paypal', 'gigneo' ),
		'payoneer' => esc_html__( 'Payoneer', 'gigneo' ),
		'bank'     => esc_html__( 'Bank', 'gigneo' ),
		'custom'   => esc_html__( 'Custom', 'gigneo' ),
	);

	$active_payouts = array();
	$payouts        = gigneo_option( '_gigneo_ts_payment_withdraw_payouts', array( 'paypal', 'payoneer' ) );

	if ( ! empty( $payouts ) && is_array( $payouts ) ) {
		foreach ( $payouts as $payout ) {
			if ( isset( $payout_list[ $payout ] ) ) {
				$active_payouts[ $payout ] = $payout_list[ $payout ];
			}
		}
	}

	if ( isset( $active_payouts['custom'] ) ) {
		unset( $active_payouts['custom'] );
	}

	return apply_filters( 'gigneo_withdrawal_payout_list', $active_payouts );
}

function gigneo_payout_logo_url( $type = '' ) {

	$payout_logo = '';

	// Check assets folder first
	$payout_logo_path = GIGNEO_DIR_PATH . "assets/img/icon/$type.png";
	$payout_logo_url  = GIGNEO_DIR_URL . "assets/img/icon/$type.png";
	$payout_logo      = file_exists( $payout_logo_path ) ? $payout_logo_url : '';

	// Fallback to default payout logo
	if ( empty( $payout_logo ) ) {
		$payout_logo = GIGNEO_DIR_URL . 'assets/img/icon/payout-default.png';
	}

	return apply_filters( 'gigneo_payout_logo_url', $payout_logo, $type );
}

/**
 * Retrieve the list of bank fields for withdrawal payouts.
 *
 * @return array The array of bank fields.
 * @since 0.0.1
 */
function gigneo_payout_bank_fields() {
	$fields = array(
		'name'           => esc_html__( 'Bank name', 'gigneo' ),
		'acc_name'       => esc_html__( 'Account name', 'gigneo' ),
		'acc_number'     => esc_html__( 'Account number', 'gigneo' ),
		'branch_code'    => esc_html__( 'Branch code', 'gigneo' ),
		'routing_number' => esc_html__( 'Routing number', 'gigneo' ),
		'iban'           => esc_html__( 'IBAN', 'gigneo' ),
		'bic_swift'      => esc_html__( 'BIC/SWIFT', 'gigneo' ),
	);
	return apply_filters( 'gigneo_payout_bank_fields', $fields );
}

function gigneo_profile_payouts( $profile_id = '', $status = '' ) {
	if ( empty( $profile_id ) ) {
		$profile_id = gigneo_active_profile_id();
	}

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

	$active_payout_list        = gigneo_withdrawal_payout_list();
	$profile_payouts           = gigneo_post_meta( $profile_id, '_gigneo_pm_payouts' );
	$withdrawal_fee_percentage = gigneo_option( '_gigneo_ts_withdrawal_percentage_fee' );
	$withdrawal_fee_fixed      = gigneo_option( '_gigneo_ts_withdrawal_fixed_amount_fee' );

	$payouts_data = array();

	if ( empty( $profile_payouts ) || ! is_array( $profile_payouts ) ) {
		return $payouts_data;
	}

	foreach ( $profile_payouts as $profile_payout ) {

		$type     = $profile_payout['type'] ?? '';
		$p_status = $profile_payout['status'] ?? '';

		if ( empty( $type ) || ! isset( $active_payout_list[ $type ] ) ) {
			continue;
		}

		// Filter by status
		if ( 'active' === $status && 'active' !== $p_status ) {
			continue;
		}
		if ( 'inactive' === $status && 'inactive' !== $p_status ) {
			continue;
		}

		// Base fees
		$fee_percentage = floatval( $withdrawal_fee_percentage );
		$fee_fixed      = floatval( $withdrawal_fee_fixed );

		// Apply tier overrides (overwrite base values)
		if ( in_array( 'withdrawal_fee', gigneo_active_modules(), true ) ) {
			$tier_fees      = apply_filters(
				'gigneo_withdrawal_fee_tier_override',
				array(
					'percentage'   => $fee_percentage,
					'fixed_amount' => $fee_fixed,
				),
				$type
			);
			$fee_percentage = isset( $tier_fees['percentage'] ) ? floatval( $tier_fees['percentage'] ) : $fee_percentage;
			$fee_fixed      = isset( $tier_fees['fixed_amount'] ) ? floatval( $tier_fees['fixed_amount'] ) : $fee_fixed;
		}
		// Create description
		if ( $fee_percentage > 0 && $fee_fixed > 0 ) {
			$description = sprintf(
				// translators: 1: fee percentage, 2: fixed fee amount
				esc_html__( 'Fee may apply: %1$s%% + %2$s per withdrawal.', 'gigneo' ),
				$fee_percentage,
				gigneo_price_format( $fee_fixed, true )
			);
		} elseif ( $fee_percentage > 0 ) {
			$description = sprintf(
				// translators: %s% is fee percentage
				esc_html__( 'Fee may apply: %s%% per withdrawal.', 'gigneo' ),
				$fee_percentage
			);
		} elseif ( $fee_fixed > 0 ) {
			$description = sprintf(
			// translators: %s is fixed fee amount
				esc_html__( 'Fee may apply: %s per withdrawal.', 'gigneo' ),
				gigneo_price_format( $fee_fixed, true )
			);
		} else {
			$description = esc_html__( 'No withdrawal fee applies.', 'gigneo' );
		}

		// Save using payout type as array key
		$payouts_data[] = array(
			'type'        => $type,
			'status'      => $p_status,
			'label'       => $profile_payout['label'] ?? '',
			'title'       => $active_payout_list[ $type ],
			'logo'        => gigneo_payout_logo_url( $type ),
			'description' => $description,
			'commission'  => array(
				'percentage' => $fee_percentage,
				'fixed'      => $fee_fixed,
			),
		);
	}

	return apply_filters( 'gigneo_profile_payouts', $payouts_data, $profile_id );
}

/**
 * Get the available delivery time types.
 *
 * This function returns an array of delivery time types.
 *
 * @param string $type Optional. The type of delivery time to retrieve.
 * @return array The array of delivery time types.
 * @since 0.0.1
 */
function gigneo_delivery_time_types( $type = '', $none = true ) {
	$types = array(
		'hour' => esc_html__( 'Hour', 'gigneo' ),
		'day'  => esc_html__( 'Day', 'gigneo' ),
		'week' => esc_html__( 'Week', 'gigneo' ),
		'year' => esc_html__( 'Year', 'gigneo' ),
	);

	if ( $none ) {
		$types['none'] = esc_html__( 'None', 'gigneo' );
	}

	$types = apply_filters( 'gigneo_delivery_time_types', $types );

	if ( ! empty( $type ) && isset( $types[ $type ] ) ) {
		return $types[ $type ];
	}

	return $types;
}


function gigneo_delivery_time( $value = 0, $type = 'day' ) {

	$value = intval( $value );

	if ( $value <= 0 ) {
		return '';
	}

	// Translation-ready plural labels
	$type_label = match ( $type ) {
		'hour' => _n( 'Hour', 'Hours', $value, 'gigneo' ),
		'week' => _n( 'Week', 'Weeks', $value, 'gigneo' ),
		'year' => _n( 'Year', 'Years', $value, 'gigneo' ),
		default => _n( 'Day', 'Days', $value, 'gigneo' ),
	};

	return sprintf(
		// translators: 1: number, 2: time label
		__( '%1$d %2$s', 'gigneo' ),
		esc_html( $value ),
		esc_html( $type_label )
	);
}

/**
 * Retrieve the platform type.
 *
 * This function returns the platform type set in the options.
 *
 * @return string The platform type.
 * @since 0.0.1
 */
function gigneo_platform_type() {
	$platform_type = gigneo_option( '_gigneo_ts_platform_type', 'both' );
	return apply_filters( 'gigneo_platform_type', $platform_type );
}

/**
 * Format a price.
 *
 * @param float $number The number to format.
 * @param bool $include_symbol Whether to include the currency symbol. Default is false.
 * @return string The formatted price.
 * @since 0.0.1
 */
function gigneo_price_format( $number, $include_symbol = false, $decimals = true ) {

	$number             = (float) $number;
	$negative           = $number < 0;
	$number             = abs( $number );
	$decimals           = $decimals ? wc_get_price_decimals() : 2;
	$decimal_separator  = wc_get_price_decimal_separator();
	$thousand_separator = wc_get_price_thousand_separator();

	$formatted_number = number_format( $number, $decimals, $decimal_separator, $thousand_separator );
	if ( ! $include_symbol ) {
		if ( $negative ) {
			$formatted_number = '-' . $formatted_number;
		}
		return apply_filters( 'gigneo_price_format', $formatted_number );
	}

	$currency_symbol   = get_woocommerce_currency_symbol();
	$currency_symbol   = html_entity_decode( $currency_symbol );
	$currency_position = get_option( 'woocommerce_currency_pos' );

	$formatted_price = match ( $currency_position ) {
		'right' => sprintf( '%1$s%2$s', $formatted_number, $currency_symbol ),
		'left_space' => sprintf( '%1$s %2$s', $currency_symbol, $formatted_number ),
		'right_space' => sprintf( '%1$s %2$s', $formatted_number, $currency_symbol ),
		default => sprintf( '%1$s%2$s', $currency_symbol, $formatted_number ),
	};

	if ( $negative ) {
		$formatted_price = '-' . $formatted_price;
	}

	return apply_filters( 'gigneo_price_format', $formatted_price );
}


/**
 * Retrieve the placeholder image URL.
 *
 * This function returns the URL of the placeholder image used in the Gigneo plugin.
 *
 * @return string The URL of the placeholder image.
 * @since 0.0.1
 */
function gigneo_placeholder_url() {
	$url = GIGNEO_DIR_URL . 'assets/img/placeholder.png';
	return apply_filters( 'gigneo_placeholder_url', $url );
}

/**
 * Retrieve the attachment image source.
 *
 * This function retrieves the image HTML for a given attachment ID and size.
 * If the image is not found, it falls back to a default image based on type or a placeholder.
 *
 * @param int $attachment_id The ID of the attachment.
 * @param string $size The size of the image. Default is 'medium'.
 * @param string $type The type of image. Default is an empty string.
 * @param string $classes Additional CSS class for the image. Default is an empty string.
 * @return void
 * @since 0.0.1
 */
function gigneo_attachment_image_src( $attachment_id, $size = 'medium', $type = '', $classes = '', $placeholder_url = '', $profile_id = 0 ) {

	// Try to get the image HTML for the provided attachment ID
	$image_html = wp_get_attachment_image( $attachment_id, $size, false, array( 'class' => $classes ) );

	if ( $image_html ) {
		echo wp_kses_post( $image_html );
		return;
	}

	// Fallback by type
	$default_image_id = '';

	if ( 'profile' === $type ) {
		$profile_type = gigneo_profile_type( $profile_id );
		if ( 'freelancer' === $profile_type || 'employer' === $profile_type ) {
			$default_image_id = gigneo_option( "_gigneo_ts_{$profile_type}_default_profile_image" );
		}

		if ( empty( $default_image_id ) ) {
			$user_id    = ! empty( $profile_id ) ? gigneo_post_meta( $profile_id, '_gigneo_pm_user_id' ) : get_current_user_id();
			$image_url  = get_avatar_url( $user_id );
			$image_html = sprintf(
				// translators: 1: Image URL, 2: Image alt, 3: classes
				'<img src="%1$s" alt="%2$s" class="%3$s" />',
				esc_url( ! empty( $image_url ) ? $image_url : gigneo_placeholder_url() ),
				esc_attr__( 'Profile image', 'gigneo' ),
				esc_attr( $classes )
			);
			echo wp_kses_post( $image_html );

			return;
		}
	}

	if ( ! empty( $default_image_id ) ) {
		$image_html = wp_get_attachment_image( $default_image_id, $size, false, array( 'class' => $classes ) );
		if ( $image_html ) {
			echo wp_kses_post( $image_html );
			return;
		}
	}

	// Final fallback to placeholder URL
	$image_html = sprintf(
		// translators: 1: Image URL, 2: Image alt, 3: classes
		'<img src="%1$s" alt="%2$s" class="%3$s" />',
		esc_url( ! empty( $placeholder_url ) ? $placeholder_url : gigneo_placeholder_url() ),
		esc_attr__( 'Placeholder', 'gigneo' ),
		esc_attr( $classes )
	);
	echo wp_kses_post( $image_html );
}

/**
 * Retrieve the wallet balance for a given profile ID.
 *
 * This function calculates the total amount in the wallet for a specific profile ID.
 *
 * @param int $profile_id The ID of the profile. Default is empty.
 * @return float The total wallet amount.
 * @since 0.0.1
 */
function gigneo_wallet_balance( $profile_id, $requested_withdrawals = true ) {
	$amount = 0;

	if ( ! empty( $profile_id ) ) {
		global $wpdb;

		// Get total earnings (invoices) with proper decimal precision
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$total = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM(CAST(pm.meta_value AS DECIMAL(12,2))) as total
                 FROM {$wpdb->posts} p
                 INNER JOIN {$wpdb->postmeta} pm   ON p.ID = pm.post_id   AND pm.meta_key   = '_gigneo_pm_total'
                 INNER JOIN {$wpdb->postmeta} ppm  ON p.ID = ppm.post_id  AND ppm.meta_key  = '_gigneo_pm_profile_id' AND ppm.meta_value = %d
                 WHERE p.post_type = 'gigneo_invoice'
                   AND p.post_status IN ('completed','paid','refunded','withdrawn')",
				$profile_id
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		$amount = floatval( $total ?? 0 );

		// Subtract pending withdrawal requests if enabled
		if ( $requested_withdrawals ) {
			$requested_amount = gigneo_requested_withdrawals( $profile_id );
			$amount          -= floatval( $requested_amount );
		}
	}

	return apply_filters( 'gigneo_wallet_balance', $amount, $requested_withdrawals );
}

/**
 * Get available credits for a profile.
 *
 * Retrieves the available credits for a given profile ID.
 *
 * @param int|string $profile_id Profile ID.
 * @return float Available credits amount.
 * @since 0.0.1
 */
function gigneo_available_credits( $profile_id ) {
	$credits = 0;
	if ( ! empty( $profile_id ) ) {
		global $wpdb;
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$total = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM( CAST( pm.meta_value AS SIGNED ) )
             FROM {$wpdb->posts} p
             INNER JOIN {$wpdb->postmeta} pm  ON p.ID = pm.post_id
             INNER JOIN {$wpdb->postmeta} ppm ON p.ID = ppm.post_id
             INNER JOIN {$wpdb->postmeta} ppt ON p.ID = ppt.post_id
             WHERE p.post_type = 'gigneo_invoice'
               AND p.post_status IN ( 'completed', 'paid' )
               AND pm.meta_key  = '_gigneo_pm_credit'
               AND ppm.meta_key = '_gigneo_pm_profile_id'
               AND ppm.meta_value = %d
               AND ppt.meta_key = '_gigneo_pm_type'
               AND ppt.meta_value IN ('credit','bulk_credit','monthly_credit','credit_plan' , 'job_creation', 'service_creation' , 'proposal_submission')",
				$profile_id
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$credits = floatval( $total );
	}
	return apply_filters( 'gigneo_available_credits', $credits, $profile_id );
}

/**
 * Get total requested withdrawals for a profile.
 *
 * @param int|string $profile_id Profile ID.
 * @return float Total requested withdrawals amount.
 * @since 0.0.1
 */
function gigneo_requested_withdrawals( $profile_id ) {
	$total_requested = 0;

	if ( ! empty( $profile_id ) ) {
		global $wpdb;

		// Fixed: Use DECIMAL(12,2) instead of SIGNED to preserve cents
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$total = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM(CAST(pm.meta_value AS DECIMAL(12,2))) as total
                 FROM {$wpdb->posts} p
                 INNER JOIN {$wpdb->postmeta} pm  ON p.ID = pm.post_id  AND pm.meta_key  = '_gigneo_pm_total'
                 INNER JOIN {$wpdb->postmeta} ppm ON p.ID = ppm.post_id AND ppm.meta_key = '_gigneo_pm_profile_id' AND ppm.meta_value = %d
                 WHERE p.post_type = 'gigneo_withdrawal'
                   AND p.post_status IN ('publish', 'in-progress', 'on-hold')",
				$profile_id
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		$total_requested = floatval( $total ?? 0 );
	}

	return apply_filters( 'gigneo_requested_withdrawals', $total_requested, $profile_id );
}

/**
 * Check if the credit system is enabled.
 *
 * This function checks if the credit system is enabled based on the profile type and platform settings.
 *
 * @param string $profile_type Optional. The profile type to check. Default is empty.
 * @return bool True if the credit system is enabled, false otherwise.
 * @since 0.0.1
 */
function gigneo_has_credit_system( $profile_type = '' ) {

	if ( empty( $profile_type ) ) {
		$profile_type = gigneo_profile_type();
	}

	$platform_type = gigneo_platform_type();

	$status = false;

	//Service Creation
	if ( 'jobs' !== $platform_type && 'freelancer' === $profile_type ) {
		$service_creation_type    = gigneo_option( '_gigneo_ts_service_creation_type', 'paid' );
		$service_creation_credits = gigneo_option( '_gigneo_ts_service_creation_credits', '10' );

		if ( 'paid' === $service_creation_type && ! empty( $service_creation_credits ) && floatval( $service_creation_credits ) > 0 ) {
			$status = true;
		}
	}

	//Job Creation
	if ( 'services' !== $platform_type ) {

		if ( 'freelancer' === $profile_type ) {
			$job_apply_type    = gigneo_option( '_gigneo_ts_job_apply_type', 'paid' );
			$job_apply_credits = gigneo_option( '_gigneo_ts_job_apply_credits', '10' );

			if ( 'paid' === $job_apply_type && ! empty( $job_apply_credits ) && floatval( $job_apply_credits ) > 0 ) {
				$status = true;
			}
		}

		if ( 'employer' === $profile_type ) {
			$job_creation_type    = gigneo_option( '_gigneo_ts_job_creation_type', 'paid' );
			$job_creation_credits = gigneo_option( '_gigneo_ts_job_creation_credits', '10' );

			if ( 'paid' === $job_creation_type && ! empty( $job_creation_credits ) && floatval( $job_creation_credits ) > 0 ) {
				$status = true;
			}
		}
	}

	return apply_filters( 'gigneo_has_credit_system', $status );
}

/**
 * Convert a hex color code to RGB format.
 *
 * This function converts a hex color code to RGB format, either as a string or an array.
 *
 * @param string $hex The hex color code (e.g., '#ff0000').
 * @param bool $rgb Whether to return the result in 'rgb(r, g, b)' format. Default is true.
 * @return string|array The RGB representation of the hex color code.
 * @since 0.0.1
 */
function gigneo_hex_to_rgb( $hex, $rgb = true, $opacity = 1 ) {
	// Remove '#' if present
	$hex = ltrim( $hex, '#' );

	// Ensure hex is 6 characters
	if ( 3 === strlen( $hex ) ) {
		$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
	}

	// Convert hex to decimal
	$r = hexdec( substr( $hex, 0, 2 ) );
	$g = hexdec( substr( $hex, 2, 2 ) );
	$b = hexdec( substr( $hex, 4, 2 ) );

	if ( $rgb ) {
		return esc_html( "rgb($r, $g, $b, $opacity)" );
	}

	if ( empty( $opacity ) ) {
		return esc_html( "$r, $g, $b" );
	}

	return esc_html( "$r, $g, $b, $opacity" );
}

/**
 * Generate an attachment object for a cropped image.
 *
 * This function creates an attachment object for a cropped image based on the parent attachment ID.
 *
 * @param string $cropped The path to the cropped image.
 * @param int $parent_attachment_id The ID of the parent attachment.
 * @return array The attachment object.
 * @since 0.0.1
 */
function gigneo_attachment_object( $cropped, $parent_attachment_id ) {
	$parent     = get_post( $parent_attachment_id );
	$parent_url = wp_get_attachment_url( $parent->ID );
	$url        = str_replace( basename( $parent_url ), basename( $cropped ), $parent_url );

	$size       = getimagesize( $cropped );
	$image_type = ( $size ) ? $size['mime'] : 'image/jpeg';

	$object = array(
		'ID'             => $parent_attachment_id,
		'post_title'     => basename( $cropped ),
		'post_mime_type' => $image_type,
		'guid'           => $url,
		'context'        => 'custom-header',
	);

	return $object;
}

/**
 * Register a notice.
 *
 * This function registers a notice with the specified arguments.
 *
 * @param array $args The arguments for the notice.
 * @return array The registered notices.
 * @since 0.0.1
 */
function gigneo_register_notice( $args = array() ) {
	static $gigneo_notices = array();

	if ( ! empty( $args ) && is_array( $args ) ) {
		$defaults = array(
			'id'               => uniqid( 'gigneo_notice_' ),
			'title'            => '',
			'description'      => '',
			'icon'             => '',
			'close'            => true,
			'primary_button'   => array(
				'label'  => '',
				'icon'   => '',
				'url'    => '',
				'target' => '_self',
				'id'     => '',
				'class'  => '',
				'attr'   => array(),
			),
			'secondary_button' => array(
				'label'  => '',
				'icon'   => '',
				'url'    => '',
				'target' => '_self',
				'id'     => '',
				'class'  => '',
				'attr'   => array(),
			),
			'source'           => array(),
			'ref'              => array(),
			'excludes'         => array(),
			'dismissible'      => false,
			'type'             => 'info',
			'user_type'        => array( 'freelancer', 'employer' ),
		);

		$notice = wp_parse_args( $args, $defaults );

		if ( ! empty( $notice['id'] ) && is_string( $notice['id'] ) ) {
			$gigneo_notices[ $notice['id'] ] = $notice;
		}
	}

	return $gigneo_notices;
}


/**
 * Retrieve the list of notices.
 *
 * This function retrieves the list of notices, including dismissed notices if specified.
 *
 * @param int $profile_id The ID of the profile. Default is empty.
 * @param bool $dismissed Whether to include dismissed notices. Default is false.
 * @return array The array of notices.
 * @since 0.0.1
 */
function gigneo_get_notices( $profile_id = '', $dismissed = false ) {
	if ( empty( $profile_id ) ) {
		$profile_id = gigneo_active_profile_id();
	}

	global $gigneo_args;

	$all_notices  = gigneo_register_notice();
	$profile_type = gigneo_profile_type( $profile_id );
	$notices      = array();

	$current_source = isset( $gigneo_args['source'] ) ? sanitize_text_field( wp_unslash( $gigneo_args['source'] ) ) : '';
	$current_ref    = isset( $gigneo_args['ref'] ) ? sanitize_text_field( wp_unslash( $gigneo_args['ref'] ) ) : '';

	foreach ( $all_notices as $notice_id => $notice ) {
		// Dismissed check
		$is_dismissed = gigneo_post_meta( $profile_id, '_gigneo_notice_dismissed_' . $notice_id );
		if ( ! $dismissed && $notice['dismissible'] && $is_dismissed ) {
			continue;
		}

		// User type check
		if ( ! in_array( $profile_type, (array) $notice['user_type'], true ) ) {
			continue;
		}

		// Prepare all array fields safely
		$source_list     = (array) ( $notice['source'] ?? array() );
		$ref_list        = (array) ( $notice['ref'] ?? array() );
		$exclude_data    = (array) ( $notice['excludes'] ?? array() );
		$exclude_sources = (array) ( $exclude_data['source'] ?? array() );
		$exclude_refs    = (array) ( $exclude_data['ref'] ?? array() );

		// Exclusion check
		if (
			in_array( $current_source, $exclude_sources, true ) ||
			in_array( $current_ref, $exclude_refs, true )
		) {
			continue;
		}

		// Source match check (if sources defined)
		if ( ! empty( $source_list ) && ! in_array( $current_source, $source_list, true ) ) {
			continue;
		}

		// Ref match check (if refs defined)
		if ( ! empty( $ref_list ) && ! in_array( $current_ref, $ref_list, true ) ) {
			continue;
		}

		// Passed all filters
		$notices[ $notice_id ] = $notice;
	}

	return apply_filters( 'gigneo_get_notices', $notices, $profile_id, $dismissed );
}

/**
 * Generate a email verification link.
 *
 * This function generates a unique email verification link.
 *
 * @param int $user_id The ID of the user to verify.
 * @return string The generated verification link.
 * @since 0.0.1
 */
function gigneo_generate_email_verify_link( $user_id ) {
	$user_id = absint( $user_id );

	if ( ! $user_id || ! gigneo_user_have_access( $user_id ) ) {
		return '';
	}

	$ttl     = (int) apply_filters( 'gigneo_email_verification_token_ttl', 48 * HOUR_IN_SECONDS );
	$expires = time() + max( $ttl, HOUR_IN_SECONDS );
	$nonce   = bin2hex( random_bytes( 16 ) );
	$payload = $user_id . ':' . $expires . ':' . $nonce;
	$token   = gigneo_encode_email_verification_token( $payload . ':' . hash_hmac( 'sha256', $payload, wp_salt( 'gigneo_email_verification' ) ) );

	update_user_meta( $user_id, '_gigneo_um_email_verification_token', $nonce );
	update_user_meta( $user_id, '_gigneo_um_email_verification_expires', $expires );
	update_user_meta( $user_id, '_gigneo_um_email_verified', 0 );

	$dashboard_template = gigneo_option( '_gigneo_ts_template_dashboard' );
	$dashboard_url      = ! empty( $dashboard_template ) ? get_permalink( $dashboard_template ) : site_url();

	return add_query_arg(
		array(
			'source'          => 'setting',
			'ref'             => 'security',
			'gn_verify_email' => 1,
			'token'           => $token,
		),
		$dashboard_url
	);
}

/**
 * Encode an email verification token for use in URLs.
 *
 * @param string $token Raw token string.
 * @return string URL-safe encoded token.
 * @since 0.0.1
 */
function gigneo_encode_email_verification_token( $token ) {
	return rtrim( strtr( base64_encode( $token ), '+/', '-_' ), '=' ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- URL-safe encoding for signed verification tokens in email links.
}

/**
 * Decode a URL-safe email verification token.
 *
 * @param string $token URL-safe encoded token.
 * @return string|false Raw token string or false on failure.
 * @since 0.0.1
 */
function gigneo_decode_email_verification_token( $token ) {
	$token   = strtr( $token, '-_', '+/' );
	$padding = strlen( $token ) % 4;

	if ( $padding ) {
		$token .= str_repeat( '=', 4 - $padding );
	}

    // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- Decodes URL-safe signed verification tokens from email links.
	$decoded = base64_decode( $token, true );

	return false !== $decoded ? $decoded : false;
}

/**
 * Verify a signed email verification token and consume it atomically.
 *
 * @param string $token Signed verification token from the verification link.
 * @return int|false Verified user ID on success, false otherwise.
 * @since 0.0.1
 */
function gigneo_verify_email_verification_token( $token ) {
	$decoded = gigneo_decode_email_verification_token( $token );

	if ( false === $decoded ) {
		return false;
	}

	$parts = explode( ':', $decoded, 4 );

	if ( 4 !== count( $parts ) ) {
		return false;
	}

	list( $user_id, $expires, $nonce, $signature ) = $parts;
	$user_id                                       = absint( $user_id );
	$expires                                       = absint( $expires );

	if ( ! $user_id || ! $expires || ! $nonce || ! $signature || $expires < time() ) {
		return false;
	}

	$payload  = $user_id . ':' . $expires . ':' . $nonce;
	$expected = hash_hmac( 'sha256', $payload, wp_salt( 'gigneo_email_verification' ) );

	if ( ! hash_equals( $expected, $signature ) ) {
		return false;
	}

	$user = get_userdata( $user_id );

	if ( ! $user || ! gigneo_user_have_access( $user_id ) ) {
		return false;
	}

	$stored_expires = (int) get_user_meta( $user_id, '_gigneo_um_email_verification_expires', true );

	if ( ! $stored_expires || $stored_expires < time() || $stored_expires !== $expires ) {
		delete_user_meta( $user_id, '_gigneo_um_email_verification_token' );
		delete_user_meta( $user_id, '_gigneo_um_email_verification_expires' );

		return false;
	}

	global $wpdb;

    // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value
	$deleted = $wpdb->delete(
		$wpdb->usermeta,
		array(
			'user_id'    => $user_id,
			'meta_key'   => '_gigneo_um_email_verification_token',
			'meta_value' => $nonce,
		),
		array( '%d', '%s', '%s' )
	);
    // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.SlowDBQuery.slow_db_query_meta_key,WordPress.DB.SlowDBQuery.slow_db_query_meta_value

	if ( ! $deleted ) {
		return false;
	}

	delete_user_meta( $user_id, '_gigneo_um_email_verification_expires' );

	return $user_id;
}

/**
 * Retrieve the gigneo upload directory path for Gigneo.
 *
 * This function returns the upload directory path for Gigneo, allowing for customization through filters.
 *
 * @param string $path Optional. Additional path to append. Default is an empty string.
 * @return string The upload directory path for Gigneo.
 * @since 0.0.1
 */
function gigneo_upload_dir_path( $path = '' ) {
	$upload_dir = wp_upload_dir();
	$dir_path   = apply_filters( 'gigneo_upload_dir_path', $upload_dir['basedir'] . '/gigneo' );

	return trailingslashit( $dir_path ) . ltrim( $path, '/' );
}

/**
 * Retrieve the gigneo upload directory url for Gigneo.
 *
 * This function returns the upload directory path for Gigneo, allowing for customization through filters.
 *
 * @param string $path Optional. Additional path to append. Default is an empty string.
 * @return string The upload directory path for Gigneo.
 * @since 0.0.1
 */
function gigneo_upload_dir_url( $path = '' ) {
	$upload_dir = wp_upload_dir();
	$dir_url    = apply_filters( 'gigneo_upload_dir_url', $upload_dir['baseurl'] . '/gigneo' );

	return trailingslashit( $dir_url ) . ltrim( $path, '/' );
}

/**
 * Move an uploaded file securely using WP_Filesystem.
 *
 * @param string $tmp_name The temporary file name.
 * @param string $file_path The destination file path.
 * @return bool True on success, false on failure.
 * @since 0.0.1
 */
function gigneo_move_uploaded_file( $tmp_name, $file_path ) {
	global $wp_filesystem;
	if ( empty( $wp_filesystem ) ) {
		require_once ABSPATH . '/wp-admin/includes/file.php';
		WP_Filesystem();
	}

	return $wp_filesystem->move( $tmp_name, $file_path, true );
}

/**
 * Retrieve a list of pages.
 *
 * This function retrieves a list of pages from the WordPress database and returns them as an associative array.
 *
 * @param bool $none Optional. Whether to include a "None" option. Default is true.
 * @return array The array of pages.
 * @since 0.0.1
 */
function gigneo_get_pages( $none = true ) {
	$page_list = array();
	$all_pages = get_pages();

	if ( $none ) {
		$page_list[''] = esc_html__( 'None', 'gigneo' );
	}

	foreach ( $all_pages as $page_item ) {
		$page_list[ $page_item->ID ] = $page_item->post_title;
	}

	return apply_filters( 'gigneo_get_pages', $page_list );
}

function gigneo_get_posts( $post_type = '', $none = true ) {

	$page_list = array();

	// Get posts based on post type
	$args = array(
		'numberposts' => -1,
		'post_type'   => ! empty( $post_type ) ? $post_type : 'any',
		'post_status' => 'publish',
	);

	$all_posts = get_posts( $args );

	// Add "None" option
	if ( $none ) {
		$page_list['none'] = esc_html__( 'None', 'gigneo' );
	}

	// Fill list
	foreach ( $all_posts as $post_item ) {
		$page_list[ $post_item->ID ] = $post_item->post_title;
	}

	return apply_filters( 'gigneo_get_posts', $page_list );
}

/**
 * Get a list of terms from a taxonomy using WP_Term_Query.
 *
 * @param array $args Arguments for WP_Term_Query.
 * @return array Associative array of term slugs => term names.
 */
function gigneo_term_list( $args = array(), $none = true ) {
	$term_list = array();

	if ( $none ) {
		$term_list['none'] = esc_html__( 'None', 'gigneo' );
	}

	$defaults = array(
		'taxonomy'   => 'gigneo_facet',
		'hide_empty' => false,
	);
	$args     = wp_parse_args( $args, $defaults );

	$terms = new WP_Term_Query( $args );

	if ( ! is_wp_error( $terms ) && ! empty( $terms->terms ) ) {
		foreach ( $terms->terms as $term ) {
			$term_list[ $term->slug ] = html_entity_decode( $term->name );
		}
	}

	return apply_filters( 'gigneo_term_list', $term_list, $args );
}

/**
 * Sanitize term slugs and keep only those present in an allowlist.
 *
 * @param array|string $slugs        Raw slug(s) from user input.
 * @param array        $allowed_list Associative array of slug => label from gigneo_term_list().
 * @return array Reindex array of allowed slugs.
 */
function gigneo_filter_term_slugs( $slugs, $allowed_list ) {
	if ( ! is_array( $slugs ) ) {
		$slugs = array( $slugs );
	}

	$slugs = array_values( array_filter( array_map( 'sanitize_text_field', $slugs ) ) );

	if ( empty( $slugs ) || empty( $allowed_list ) ) {
		return array();
	}

	$filtered = array();
	foreach ( $slugs as $slug ) {
		if ( array_key_exists( $slug, $allowed_list ) ) {
			$filtered[] = $slug;
		}
	}

	return $filtered;
}

/**
 * Get verified users.
 *
 * Returns an array of verified user IDs.
 *
 * @since 0.0.1
 *
 * @return array.
 */
function gigneo_verified_users( $profile_type = '' ) {
	global $wpdb;

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

	$users_table    = $wpdb->users;
	$usermeta_table = $wpdb->usermeta;
	$posts_table    = $wpdb->posts;
	$postmeta_table = $wpdb->postmeta;

	$profile_key = "_gigneo_um_{$profile_type}_profile_id";

	$with_identity = apply_filters( 'gigneo_verified_users_with_identity', false, $profile_type );

	if ( $with_identity ) {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$user_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT DISTINCT u.ID
				FROM %i u
				INNER JOIN %i um_email
					ON u.ID = um_email.user_id
					AND um_email.meta_key = '_gigneo_um_email_verified'
					AND um_email.meta_value = '1'
				INNER JOIN %i um_profile
					ON u.ID = um_profile.user_id
					AND um_profile.meta_key = %s
				INNER JOIN %i p
					ON p.ID = um_profile.meta_value
				INNER JOIN %i pm_identity
					ON pm_identity.post_id = p.ID
					AND pm_identity.meta_key = '_gigneo_pm_identity_verification'
					AND pm_identity.meta_value = 'verified'
				WHERE um_email.meta_value IS NOT NULL
					AND um_email.meta_value != ''",
				$users_table,
				$usermeta_table,
				$usermeta_table,
				$profile_key,
				$posts_table,
				$postmeta_table
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	} else {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$user_ids = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT DISTINCT u.ID
				FROM %i u
				INNER JOIN %i um_email
					ON u.ID = um_email.user_id
					AND um_email.meta_key = '_gigneo_um_email_verified'
					AND um_email.meta_value = '1'
				INNER JOIN %i um_profile
					ON u.ID = um_profile.user_id
					AND um_profile.meta_key = %s
				INNER JOIN %i p
					ON p.ID = um_profile.meta_value
				WHERE um_email.meta_value IS NOT NULL
					AND um_email.meta_value != ''",
				$users_table,
				$usermeta_table,
				$usermeta_table,
				$profile_key,
				$posts_table
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	return apply_filters( 'gigneo_profile_verified_users', $user_ids );
}

/**
 * Get email verified users.
 *
 * Returns an array of email verified user IDs.
 *
 * @since 0.0.1
 *
 * @return array.
 */
function gigneo_email_verified_users( $profile_type = '' ) {
	global $wpdb;

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

	$users_table    = $wpdb->users;
	$usermeta_table = $wpdb->usermeta;
	$posts_table    = $wpdb->posts;

	$profile_key = "_gigneo_um_{$profile_type}_profile_id";

    // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	$user_ids = $wpdb->get_col(
		$wpdb->prepare(
			"SELECT DISTINCT u.ID
				FROM %i u
				INNER JOIN %i um_email
					ON u.ID = um_email.user_id
					AND um_email.meta_key = '_gigneo_um_email_verified'
					AND um_email.meta_value = '1'
				INNER JOIN %i um_profile
					ON u.ID = um_profile.user_id
					AND um_profile.meta_key = %s
				INNER JOIN %i p
					ON p.ID = um_profile.meta_value
				WHERE um_email.meta_value IS NOT NULL
					AND um_email.meta_value != ''",
			$users_table,
			$usermeta_table,
			$usermeta_table,
			$profile_key,
			$posts_table
		)
	);
    // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

	return apply_filters( 'gigneo_email_verified_users', $user_ids );
}

/**
 * Get a verification badge SVG.
 *
 * This function gets an SVG for a verification badge and allows customization through filters.
 *
 * @param bool $should_echo Whether to echo the SVG or return it. Default is false.
 *
 * @return string The SVG markup if $should_echo is false, otherwise echoes the SVG.
 * @since 0.0.1
 */
function gigneo_verification_badge( $profile_id, $should_echo = false ) {

	if ( empty( $profile_id ) ) {
		$profile_id = gigneo_active_profile_id();
	}

	$tooltip_text = '';
	$verified     = false;

	$profile_type = gigneo_profile_type( $profile_id );
	$user_id      = get_post_field( 'post_author', $profile_id );

	$email_verified = get_user_meta( $user_id, '_gigneo_um_email_verified', true );

	// Email verification
	if ( $email_verified ) {
		$verified     = true;
		$tooltip_text = esc_html__( 'Email verified', 'gigneo' );
	}

	$verified = apply_filters( 'gigneo_is_user_verified', $verified, $profile_id, $user_id );

	if ( ! $verified ) {
		return '';
	}

	$tooltip_text = apply_filters( 'gigneo_verification_tooltip_text', $tooltip_text, $profile_id, $user_id );

	// SVG output
	$svg = '<span class="gn-verification-badge" role="img" aria-label="' . esc_attr( $tooltip_text ) . '">
		<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
			<path d="M9.02997 0.892513C8.66039 0.575735 8.47561 0.417347 8.26837 0.35698C8.08572 0.303775 7.89168 0.303775 7.70903 0.35698C7.5018 0.417347 7.31701 0.575736 6.94743 0.892514L5.91979 1.77335C5.75344 1.91594 5.67026 1.98723 5.57643 2.03801C5.49319 2.08306 5.40398 2.11606 5.31147 2.13603C5.20718 2.15854 5.09763 2.15854 4.87853 2.15854L3.73256 2.15854C3.17251 2.15854 2.89248 2.15854 2.67857 2.26753C2.49041 2.36341 2.33743 2.51639 2.24155 2.70455C2.13256 2.91846 2.13256 3.19849 2.13256 3.75854L2.13256 4.88845C2.13256 5.10569 2.13256 5.21431 2.1104 5.31781C2.09075 5.40961 2.05827 5.49819 2.01391 5.58093C1.96391 5.67421 1.89369 5.75708 1.75325 5.92282L0.876492 6.9575C0.565107 7.32498 0.409415 7.50871 0.350011 7.71438C0.297647 7.89567 0.297647 8.08808 0.350011 8.26937C0.409415 8.47503 0.565108 8.65877 0.876493 9.02624L1.75325 10.0609C1.89369 10.2267 1.96391 10.3095 2.01391 10.4028C2.05827 10.4856 2.09075 10.5741 2.1104 10.6659C2.13256 10.7694 2.13256 10.8781 2.13256 11.0953L2.13256 12.2252C2.13256 12.7853 2.13256 13.0653 2.24155 13.2792C2.33743 13.4674 2.49041 13.6203 2.67857 13.7162C2.89248 13.8252 3.17251 13.8252 3.73256 13.8252L4.87853 13.8252C5.09763 13.8252 5.20718 13.8252 5.31147 13.8477C5.40399 13.8677 5.49319 13.9007 5.57643 13.9457C5.67027 13.9965 5.75344 14.0678 5.9198 14.2104L6.94744 15.0912C7.31701 15.408 7.5018 15.5664 7.70903 15.6268C7.89169 15.68 8.08572 15.68 8.26837 15.6268C8.47561 15.5664 8.6604 15.408 9.02997 15.0912L10.0576 14.2104C10.224 14.0678 10.3071 13.9965 10.401 13.9457C10.4842 13.9007 10.5734 13.8677 10.6659 13.8477C10.7702 13.8252 10.8798 13.8252 11.0989 13.8252L12.222 13.8252C12.7821 13.8252 13.0621 13.8252 13.276 13.7162C13.4642 13.6203 13.6172 13.4674 13.713 13.2792C13.822 13.0653 13.822 12.7853 13.822 12.2252L13.822 11.102C13.822 10.8829 13.822 10.7734 13.8445 10.6691C13.8645 10.5766 13.8975 10.4874 13.9426 10.4041C13.9933 10.3103 14.0646 10.2271 14.2072 10.0608L15.0881 9.03314C15.4048 8.66356 15.5632 8.47878 15.6236 8.27154C15.6768 8.08889 15.6768 7.89485 15.6236 7.7122C15.5632 7.50496 15.4048 7.32018 15.0881 6.9506L14.2072 5.92296C14.0646 5.75661 13.9933 5.67343 13.9426 5.5796C13.8975 5.49636 13.8645 5.40715 13.8445 5.31464C13.822 5.21035 13.822 5.1008 13.822 4.8817L13.822 3.75854C13.822 3.19848 13.822 2.91846 13.713 2.70455C13.6172 2.51638 13.4642 2.3634 13.276 2.26753C13.0621 2.15854 12.7821 2.15854 12.222 2.15854L11.0989 2.15854C10.8798 2.15854 10.7702 2.15854 10.6659 2.13603C10.5734 2.11606 10.4842 2.08306 10.401 2.03801C10.3071 1.98723 10.224 1.91594 10.0576 1.77335L9.02997 0.892513Z" fill="#2E90FA"></path>
			<path d="M5.71484 8.28571L7.04818 9.61904L10.3815 6.2857" stroke="white" stroke-width="0.731365" stroke-linecap="round" stroke-linejoin="round"></path>
		</svg>
        <em class="gn-tooltip-text">' . esc_html( $tooltip_text ) . '</em>
    </span>';

	$svg = apply_filters( 'gigneo_verification_badge_html', $svg, $tooltip_text, $profile_id, $user_id );

	$allowed_html = array(
		'span' => array(
			'class'      => true,
			'role'       => true,
			'aria-label' => true,
		),
		'em'   => array(
			'class'      => true,
			'role'       => true,
			'aria-label' => true,
		),
		'svg'  => array(
			'width'   => true,
			'height'  => true,
			'viewbox' => true,
			'viewBox' => true,
			'fill'    => true,
			'xmlns'   => true,
		),
		'path' => array(
			'fill-rule' => true,
			'clip-rule' => true,
			'd'         => true,
			'fill'      => true,
		),
	);

	if ( ! $should_echo ) {
		return wp_kses( $svg, $allowed_html );
	}

	echo wp_kses( $svg, $allowed_html );
}

/**
 * Convert WordPress date format to Flatpickr date format.
 *
 * This function converts a WordPress date format string to a Flatpickr-compatible date format.
 *
 * @param string $wp_format The WordPress date format string.
 * @return string The Flatpickr date format string.
 * @since 0.0.1
 */
function gigneo_flatpickr_date_format( $wp_format ) {
	$replacements = array(
		'd' => 'd',     // Day of the month (01–31)
		'j' => 'j',     // Day of the month without leading zeros (1–31)
		'm' => 'm',     // Numeric month (01–12)
		'n' => 'n',     // Numeric month without leading zeros (1–12)
		'Y' => 'Y',     // Four-digit year
		'y' => 'y',     // Two-digit year
		'F' => 'F',     // Full month name
		'M' => 'M',     // Short month name
	);

	return strtr( $wp_format, $replacements );
}


/**
 * Validate if the post author matches the given user ID.
 *
 * This function checks if the post author matches the provided user ID.
 *
 * @param int $post_id The ID of the post to validate.
 * @param int $user_id Optional. The user ID to check against. Default is empty, which uses the current user ID.
 * @return bool True if the post author matches the user ID, false otherwise.
 * @since 0.0.1
 */
function gigneo_validate_post_author( $post_id, $user_id = '' ) {

	$status = false;

	if ( empty( $user_id ) ) {
		$user_id = get_current_user_id();
	}

	$post = get_post( $post_id );

	if ( isset( $post->post_author ) && (int) $post->post_author === (int) $user_id ) {
		$status = true;
	}

	return apply_filters( 'gigneo_validate_post_author', $status, $post_id, $user_id );
}

/**
 * Detect the browser from a user agent string.
 *
 * This function analyzes the provided user agent string and returns the detected browser name.
 *
 * @param string $user_agent The user agent string to analyze.
 * @return string The detected browser name.
 * @since 0.0.1
 */
function gigneo_detect_browser( $user_agent ) {
	$browser = 'Unknown';

	if ( str_contains( $user_agent, 'Brave' ) ) {
		$browser = 'Brave';
	} elseif ( str_contains( $user_agent, 'Chrome' ) ) {
		$browser = 'Chrome';
	} elseif ( str_contains( $user_agent, 'Firefox' ) ) {
		$browser = 'Firefox';
	} elseif ( str_contains( $user_agent, 'Opera' ) ) {
		$browser = 'Opera';
	} elseif ( str_contains( $user_agent, 'Safari' ) ) {
		$browser = 'Safari';
	} elseif ( str_contains( $user_agent, 'Edge' ) ) {
		$browser = 'Edge';
	}

	return apply_filters( 'gigneo_detect_browser', $browser, $user_agent );
}

/**
 * Retrieve a location label for a session IP.
 *
 * Free Gigneo returns an empty string (dashboard shows "—") and never contacts
 * external services. Gigneo Pro can resolve approximate city/country via IPAPI
 * when Session location lookup is enabled.
 *
 * @param string $ip The IP address to look up.
 * @param bool   $use_cache Optional. Whether to use cached results. Default true.
 *
 * @return string Location label, or empty when unavailable / not opted in.
 * @since 0.0.1
 */
function gigneo_get_ip_location( $ip, $use_cache = true ) {
	return apply_filters( 'gigneo_ip_location_result', '', $ip, $use_cache );
}

/**
 * Get the form field types.
 *
 * This function returns an array of form field types.
 *
 * @param string $type Optional. The type of form field to retrieve.
 * @return array The array of form field types.
 * @since 0.0.1
 */
function gigneo_intake_form_field_types( $type = '' ) {
	$types = array(
		'input'      => esc_html__( 'Text', 'gigneo' ),
		'text'       => esc_html__( 'Textarea', 'gigneo' ),
		'choice'     => esc_html__( 'Choice', 'gigneo' ),
		'attachment' => esc_html__( 'Attachment', 'gigneo' ),
		'date'       => esc_html__( 'Date', 'gigneo' ),
		'time'       => esc_html__( 'Time', 'gigneo' ),
	);

	$types = apply_filters( 'gigneo_intake_form_field_types', $types );

	if ( ! empty( $type ) && isset( $types[ $type ] ) ) {
		return $types[ $type ];
	}

	return $types;
}

/**
 * Count posts by status for a given post type and optional author.
 *
 * This function returns an associative array with post statuses as keys and their counts as values.
 *
 * @param string $post_type The post type to count.
 * @param int|null $profile_id Optional. The author ID to filter by. Default is null (all authors).
 * @return array Associative array of post status => count.
 * @since 0.0.1
 */
function gigneo_posts_status_count( $post_type, $profile_id = '', $order_type = '' ) {
	global $wpdb;

	if ( empty( $profile_id ) ) {
		$profile_id = gigneo_active_profile_id();
	}
	$profile_type   = gigneo_profile_type( $profile_id );
	$user_id        = gigneo_post_meta( $profile_id, '_gigneo_pm_user_id' );
	if ( empty( $user_id ) && ! empty( $profile_id ) ) {
		$user_id = get_post_field( 'post_author', $profile_id );
	}
	$order_meta_key = ( 'freelancer' === $profile_type ) ? '_gigneo_pm_freelancer_profile_id' : '_gigneo_pm_profile_id';

	if ( 'gigneo_service' === $post_type ) {

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$results = $wpdb->get_results(
			$wpdb->prepare(
				"
				SELECT p.post_status, COUNT(*) AS count
				FROM %i p
				WHERE p.post_type = %s
				AND p.post_status != 'trash'
				AND p.post_author = %d
				GROUP BY p.post_status
				",
				$wpdb->posts,
				$post_type,
				$user_id
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

	} elseif ( 'gigneo_order' === $post_type ) {

		if ( ! empty( $order_type ) ) {
			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$results = $wpdb->get_results(
				$wpdb->prepare(
					"
					SELECT p.post_status, COUNT(*) AS count
					FROM %i p
					INNER JOIN %i pm1 ON p.ID = pm1.post_id
					INNER JOIN %i pm2 ON p.ID = pm2.post_id
					WHERE p.post_type = %s
					AND p.post_status != 'trash'
					AND pm1.meta_key = %s
					AND pm1.meta_value = %d
					AND pm2.meta_key = '_gigneo_pm_order_type'
					AND pm2.meta_value = %s
					GROUP BY p.post_status
					",
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$post_type,
					$order_meta_key,
					$profile_id,
					$order_type
				)
			);
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		} else {
			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$results = $wpdb->get_results(
				$wpdb->prepare(
					"
					SELECT p.post_status, COUNT(*) AS count
					FROM %i p
					INNER JOIN %i pm ON p.ID = pm.post_id
					WHERE p.post_type = %s
					AND p.post_status != 'trash'
					AND pm.meta_key = %s
					AND pm.meta_value = %d
					GROUP BY p.post_status
					",
					$wpdb->posts,
					$wpdb->postmeta,
					$post_type,
					$order_meta_key,
					$profile_id
				)
			);
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		}

	} elseif ( 'gigneo_job' === $post_type ) {

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$results = $wpdb->get_results(
			$wpdb->prepare(
				"
				SELECT p.post_status, COUNT(*) AS count
				FROM %i p
				WHERE p.post_type = %s
				AND p.post_status != 'trash'
				AND p.post_author = %d
				GROUP BY p.post_status
				",
				$wpdb->posts,
				$post_type,
				$user_id
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

	} elseif ( 'gigneo_proposal' === $post_type ) {

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$results = $wpdb->get_results(
			$wpdb->prepare(
				"
				SELECT p.post_status, COUNT(*) AS count
				FROM %i p
				WHERE p.post_type = %s
				AND p.post_status != 'trash'
				AND p.post_author = %d
				GROUP BY p.post_status
				",
				$wpdb->posts,
				$post_type,
				$user_id
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

	} else {
		$results = array();
	}

	$output = array();
	$total  = 0;

	if ( $results ) {
		foreach ( $results as $row ) {
			$count                       = (int) $row->count;
			$output[ $row->post_status ] = $count;
			$total                      += $count;
		}
	}

	$output['all'] = $total;
	return apply_filters( 'gigneo_posts_status_count', $output, $post_type, $profile_id, $order_type );
}

/**
 * Count proposals by status for a specific job.
 *
 * @param int $job_id The ID of the job.
 * @return array Associative array of post status => count.
 */
function gigneo_get_job_proposals_status_count( $job_id ) {
	global $wpdb;

	// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	$results = $wpdb->get_results(
		$wpdb->prepare(
			"
			SELECT p.post_status, COUNT(*) AS count
			FROM %i p
			INNER JOIN %i pm ON p.ID = pm.post_id
			WHERE p.post_type = 'gigneo_proposal'
			AND p.post_status != 'trash'
			AND pm.meta_key = '_gigneo_pm_job_id'
			AND pm.meta_value = %d
			GROUP BY p.post_status
			",
			$wpdb->posts,
			$wpdb->postmeta,
			$job_id
		)
	);
	// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

	$output = array();
	$total  = 0;

	if ( $results ) {
		foreach ( $results as $row ) {
			$count                       = (int) $row->count;
			$output[ $row->post_status ] = $count;
			$total                      += $count;
		}
	}

	$output['all'] = $total;

	// Also include grouped status for frontend filter consistency
	$output['reviewed']    = ( $output['viewed'] ?? 0 ) + ( $output['shortlisted'] ?? 0 ) + ( $output['hired'] ?? 0 );
	$output['shortlisted'] = ( $output['shortlisted'] ?? 0 );

	return apply_filters( 'gigneo_get_job_proposals_status_count', $output, $job_id );
}

/**
 * Retrieves or sets the post status for Gigneo posts.
 *
 * @param string $status Optional. The status to set or retrieve. Default is an empty string.
 * @return mixed The post status value or result based on implementation.
 *
 * @since 1.0.0
 */
function gigneo_status_label( $status = '' ) {
	$statuses = array(
		'publish'     => esc_html__( 'Publish', 'gigneo' ),
		'active'      => esc_html__( 'Active', 'gigneo' ),
		'pending'     => esc_html__( 'Pending', 'gigneo' ),
		'paused'      => esc_html__( 'Paused', 'gigneo' ),
		'denied'      => esc_html__( 'Denied', 'gigneo' ),
		'disputed'    => esc_html__( 'Disputed', 'gigneo' ),
		'delivered'   => esc_html__( 'Delivered', 'gigneo' ),
		'dismissed'   => esc_html__( 'Dismissed', 'gigneo' ),
		'delayed'     => esc_html__( 'Delayed', 'gigneo' ),
		'completed'   => esc_html__( 'Completed', 'gigneo' ),
		'processing'  => esc_html__( 'Processing', 'gigneo' ),
		'cancelled'   => esc_html__( 'Cancelled', 'gigneo' ),
		'rejected'    => esc_html__( 'Rejected', 'gigneo' ),
		'approved'    => esc_html__( 'Approved', 'gigneo' ),
		'requested'   => esc_html__( 'Requested', 'gigneo' ),
		'revision'    => esc_html__( 'Revision', 'gigneo' ),
		'paid'        => esc_html__( 'Paid', 'gigneo' ),
		'refunded'    => esc_html__( 'Refunded', 'gigneo' ),
		'draft'       => esc_html__( 'Draft', 'gigneo' ),
		'open'        => esc_html__( 'Open', 'gigneo' ),
		'closed'      => esc_html__( 'Closed', 'gigneo' ),
		'in-progress' => esc_html__( 'In progress', 'gigneo' ),
		'on-hold'     => esc_html__( 'On hold', 'gigneo' ),
		'withdrawn'   => esc_html__( 'Withdrawn', 'gigneo' ),
		'viewed'      => esc_html__( 'Viewed', 'gigneo' ),
		'shortlisted' => esc_html__( 'Shortlisted', 'gigneo' ),
		'shortlist'   => esc_html__( 'Shortlist', 'gigneo' ),
		'hired'       => esc_html__( 'Hired', 'gigneo' ),
		'verified'    => esc_html__( 'Verified', 'gigneo' ),
	);

	if ( ! empty( $status ) ) {
		return apply_filters( "gigneo_status_label_{$status}", $statuses[ $status ] ?? '', $status );
	}

	return apply_filters( 'gigneo_status_label', $statuses, $status );
}

/**
 * Retrieves or sets the status class for Gigneo posts.
 *
 * @param string $status Optional. The status to set or retrieve. Default is an empty string.
 * @return mixed The post status value or result based on implementation.
 *
 * @since 1.0.0
 */
function gigneo_status_class( $status = '' ) {
	$statuses = array(
		'publish'     => 'gn-info',
		'active'      => 'gn-info',
		'pending'     => 'gn-warning',
		'paused'      => 'gn-warning',
		'denied'      => 'gn-error',
		'disputed'    => 'gn-error',
		'delivered'   => 'gn-success',
		'dismissed'   => 'gn-warning',
		'delayed'     => 'gn-warning',
		'completed'   => 'gn-success',
		'processing'  => 'gn-warning',
		'cancelled'   => 'gn-error',
		'rejected'    => 'gn-error',
		'approved'    => 'gn-success',
		'requested'   => 'gn-warning',
		'revision'    => 'gn-info',
		'paid'        => 'gn-success',
		'refunded'    => 'gn-info',
		'draft'       => 'gn-warning',
		'open'        => 'gn-warning',
		'closed'      => 'gn-warning',
		'in-progress' => 'gn-info',
		'on-hold'     => 'gn-warning',
		'withdrawn'   => 'gn-success',
		'viewed'      => 'gn-success',
		'shortlisted' => 'gn-success',
		'shortlist'   => 'gn-success',
		'hired'       => 'gn-success',
		'verified'    => 'gn-success',
	);

	if ( ! empty( $status ) ) {
		return apply_filters( "gigneo_status_class_{$status}", $statuses[ $status ] ?? '', $status );
	}

	return apply_filters( 'gigneo_status_class', $statuses, $status );
}
/**
 * Retrieves or sets the invoice type label for Gigneo invoices.
 *
 * @param string $type Optional. Specific invoice type key. If empty, returns full list.
 *
 * @return string|array The label for a specific type, or list of all types.
 *
 * @since 1.0.0
 */
function gigneo_invoice_label( $type = '' ) {

	$types = array(
		'withdraw'            => esc_html__( 'Withdrawn funds', 'gigneo' ),
		'service_order'       => esc_html__( 'Service order', 'gigneo' ),
		'job_order'           => esc_html__( 'Job order', 'gigneo' ),
		'job_commission'      => esc_html__( 'Job commission', 'gigneo' ),
		'service_commission'  => esc_html__( 'Service commission', 'gigneo' ),
		'proposal_submission' => esc_html__( 'Proposal submission', 'gigneo' ),
		'bulk_wallet'         => esc_html__( 'Wallet funds', 'gigneo' ),
		'bulk_credit'         => esc_html__( 'Credits', 'gigneo' ),
		'wallet'              => esc_html__( 'Wallet purchased', 'gigneo' ),
		'credit_spent'        => esc_html__( 'Credit spent', 'gigneo' ),
		'credit_purchased'    => esc_html__( 'Credit purchased', 'gigneo' ),
		'monthly_credit'      => esc_html__( 'Monthly credits', 'gigneo' ),
		'credit_plan'         => esc_html__( 'Credit plan', 'gigneo' ),
		'service_creation'    => esc_html__( 'Service creation', 'gigneo' ),
		'job_creation'        => esc_html__( 'Job creation', 'gigneo' ),
		'withdraw_commission' => gigneo_option(
			'_gigneo_ts_withdrawal_fee_name',
			esc_html__( 'Withdraw fee', 'gigneo' )
		),
		'freelancer_fee'      => gigneo_option(
			'_gigneo_ts_platform_fee_name',
			esc_html__( 'Platform fee', 'gigneo' )
		),
		'employer_fee'        => gigneo_option(
			'_gigneo_ts_platform_fee_name',
			esc_html__( 'Platform fee', 'gigneo' )
		),
	);

	// If returning a specific type
	if ( '' !== $type ) {
		$label = $types[ $type ] ?? '';
		return apply_filters( "gigneo_invoice_label_{$type}", $label, $type );
	}

	// Return full list
	return apply_filters( 'gigneo_invoice_label', $types );
}

/**
 * Retrieve the service type for a given ID.
 *
 * This function returns the service type associated with the provided ID.
 *
 * @param mixed $id Optional. The ID to retrieve the service type for. Default is empty.
 * @return mixed The service type value.
 * @since 0.0.1
 */
function gigneo_service_type( $id = '' ) {

	$service_type        = '';
	$service_type_option = gigneo_option( '_gigneo_ts_service_type', 'both' );

	if ( 'both' !== $service_type_option ) {
		$service_type = $service_type_option;

		return apply_filters( 'gigneo_service_type', $service_type, $id );
	}

	if ( ! empty( $id ) ) {
		$service_type_meta = gigneo_post_meta( $id, '_gigneo_pm_type' );
		$service_type      = ! empty( $service_type_meta ) ? $service_type_meta : $service_type_option;
	}

	return apply_filters( 'gigneo_service_type', $service_type, $id );
}

/**
 * Retrieve the service pricing plan by name.
 *
 * This function returns the service pricing plan associated with the provided name.
 *
 * @param string $name Optional. The name of the pricing plan to retrieve. Default is empty.
 * @return mixed The pricing plan value.
 * @since 0.0.1
 */
function gigneo_service_price_plan( $name = '' ) {
	$plans = array(
		'one'   => esc_html__( 'Basic', 'gigneo' ),
		'two'   => esc_html__( 'Standard', 'gigneo' ),
		'three' => esc_html__( 'Premium', 'gigneo' ),
	);

	if ( ! empty( $name ) && isset( $plans[ $name ] ) ) {
		return apply_filters( "gigneo_service_price_plan_{$name}", $plans[ $name ], $name );
	}

	return apply_filters( 'gigneo_service_price_plan', $plans );
}

/**
 * Calculate commission details for a given profile and order total.
 *
 * Supports both employer and freelancer commission settings.
 *
 * Priority:
 * 1. Profile tier match (if commission_tier module enabled)
 * 2. Global tier match (if commission_tier module enabled)
 * 3. Profile flat fee
 * 4. Global flat fee
 * 5. No commission
 *
 * @param float  $total           Order total amount.
 * @param string $commission_type Commission type (service, job).
 * @param int    $profile_id      Profile ID (optional).
 * @return array
 */
function gigneo_commission( float $total = 0, $commission_type = 'service', $profile_id = '' ): array {

	$commission = array(
		'fixed_amount' => 0,
		'percentage'   => 0,
		'total'        => 0,
	);

	$platform_type = gigneo_platform_type();
	$platform_type = 'job' === $platform_type ? 'job' : ( 'service' === $platform_type ? 'service' : '' );
	$fee_type      = $commission_type ?: $platform_type;

	if ( empty( $profile_id ) ) {
		$profile_id = gigneo_active_profile_id();
	}

	$profile_type = gigneo_profile_type( $profile_id );

	if ( empty( $profile_type ) ) {
		return $commission;
	}

	/**
	 * Calculate commission amount.
	 */
	$calculate = function ( $percentage, $fixed, $total ) {

		$percentage = (float) $percentage;
		$fixed      = (float) $fixed;

		return array(
			'percentage'   => $percentage,
			'fixed_amount' => $fixed,
			'total'        => ( ( $percentage / 100 ) * $total ) + $fixed,
		);
	};

	// Flat fee keys.
	$meta_percentage_key   = "_gigneo_pm_{$fee_type}_percentage_fee";
	$meta_fixed_key        = "_gigneo_pm_{$fee_type}_fixed_amount_fee";
	$option_percentage_key = "_gigneo_ts_{$profile_type}_{$fee_type}_percentage_fee";
	$option_fixed_key      = "_gigneo_ts_{$profile_type}_{$fee_type}_fixed_amount_fee";

	/**
	 * Find matching tier.
	 */
	$calculate_tier = function ( $tiers, $total ) use ( $calculate ) {

		if ( empty( $tiers ) || ! is_array( $tiers ) ) {
			return false;
		}

		foreach ( $tiers as $tier ) {

			$min        = trim( (string) ( $tier['min_order_amount'] ?? '' ) );
			$max        = trim( (string) ( $tier['max_order_amount'] ?? '' ) );
			$percentage = (float) ( $tier['percentage'] ?? 0 );
			$fixed      = (float) ( $tier['fixed_amount'] ?? 0 );

			// Skip tier if both min and max are empty.
			if ( '' === $min && '' === $max ) {
				continue;
			}

			if ( '' !== $min && $total < (float) $min ) {
				continue;
			}

			if ( '' !== $max && $total > (float) $max ) {
				continue;
			}

			return $calculate(
				$percentage,
				$fixed,
				$total
			);
		}

		return false;
	};

	/**
	 * Tier commissions.
	 */
	$tier_commission = apply_filters( 'gigneo_commission_profile_tier', false, $total, $fee_type, $profile_id );
	if ( false !== $tier_commission ) {
		return apply_filters(
			'gigneo_commission',
			$tier_commission,
			$profile_id
		);
	}

	/**
	 * Profile flat commission.
	 */
	$percentage = gigneo_post_meta( $profile_id, $meta_percentage_key );
	$fixed      = gigneo_post_meta( $profile_id, $meta_fixed_key );

	if ( $percentage > 0 || $fixed > 0 ) {
		return apply_filters(
			'gigneo_commission',
			$calculate( $percentage, $fixed, $total ),
			$profile_id
		);
	}

	// No profile tier matched & flat commission, try global tiers.
	$tier_commission = apply_filters( 'gigneo_commission_global_tier', false, $total, $fee_type, $profile_type );
	if ( false !== $tier_commission ) {
		return apply_filters(
			'gigneo_commission',
			$tier_commission,
			$profile_id
		);
	}

	/**
	 * Global flat commission.
	 */
	$percentage = gigneo_option( $option_percentage_key );
	$fixed      = gigneo_option( $option_fixed_key );

	if ( $percentage > 0 || $fixed > 0 ) {
		return apply_filters(
			'gigneo_commission',
			$calculate( $percentage, $fixed, $total ),
			$profile_id
		);
	}

	return apply_filters(
		'gigneo_commission',
		$commission,
		$profile_id
	);
}

/**
 * Convert a given value to hours based on the specified type.
 *
 * This function converts the input value to hours depending on the type provided.
 *
 * @param $value
 * @param $type
 *
 * @return float The converted value in hours.
 * @since 0.0.1
 */
function gigneo_convert_to_hours( $value, $type ) {
	return match ( strtolower( $type ) ) {
		'hour' => $value,
		'day' => $value * 24,
		'week' => $value * 7 * 24,
		'year' => $value * 365 * 24,
		default => 0,
	};
}

/**
 * Convert hours to a human-readable format of days and hours.
 *
 * This function takes a number of hours and converts it into a string representation
 * showing the equivalent number of days and remaining hours.
 *
 * @param int $hours The total number of hours to convert.
 * @return string A formatted string representing the time in days and hours.
 * @since 0.0.1
 */
function gigneo_convert_to_days( $hours ) {

	$days  = floor( $hours / 24 );
	$hours = $hours % 24;

	$output = '';
	if ( $days > 0 ) {
		$output .= sprintf(
			// translators: %s is day(s)
			esc_html( _n( '%s day', '%s days', $days, 'gigneo' ) ),
			$days
		);
	}
	if ( $hours > 0 ) {
		if ( ! empty( $output ) ) {
			$output .= ' ';
		}
		$output .= sprintf(
			// translators: %s is hour(s)
			esc_html( _n( '%s hour', '%s hours', $hours, 'gigneo' ) ),
			$hours
		);
	}

	$output = ! empty( $output ) ? $output : esc_html__( '0 hours', 'gigneo' );

	return apply_filters( 'gigneo_convert_to_days', $output, $hours );
}

/**
 * Format an ID based on the specified type.
 *
 * This function formats the given ID according to the provided type.
 *
 * @param int $id The ID to format. Default is 0.
 * @param string $type Optional. The type of formatting to apply. Default is empty.
 * @return mixed The formatted ID.
 * @since 0.0.1
 */
function gigneo_format_id( $id = 0, $type = '', $hash = true ) {

	$prefixes = array(
		'order'    => esc_html__( 'ORD', 'gigneo' ),
		'ticket'   => esc_html__( 'TCK', 'gigneo' ),
		'invoice'  => esc_html__( 'INV', 'gigneo' ),
		'service'  => esc_html__( 'SRV', 'gigneo' ),
		'job'      => esc_html__( 'JOB', 'gigneo' ),
		'report'   => esc_html__( 'REP', 'gigneo' ),
		'user'     => esc_html__( 'USR', 'gigneo' ),
		'withdraw' => esc_html__( 'WDR', 'gigneo' ),
		'plan'     => esc_html__( 'PLN', 'gigneo' ),
		'proposal' => esc_html__( 'PRP', 'gigneo' ),
	);

	// Default to first 3 letters of type if not in list
	$prefix = $prefixes[ $type ] ?? strtoupper( substr( $type, 0, 3 ) );

	// Add hash or not
	$prefix = $hash ? "#{$prefix}" : $prefix;

	// Format ID with leading zeros (6 digits)
	$formatted = sprintf( '%s-%06d', $prefix, absint( $id ) );

	return apply_filters( 'gigneo_format_id', $formatted, $type, $id, $hash );
}


/**
 * Retrieve the order stages for a given order and role.
 *
 * This function returns the stages of an order based on the order ID and optional role.
 *
 * @param int $order_id The ID of the order.
 * @param string $role Optional. The role to filter stages by.
 * @return array The array of order stages.
 * @since 0.0.1
 */
function gigneo_order_stages( $order_id, $role = '' ) {

	if ( empty( $role ) ) {
		$role = gigneo_profile_type();
	}

	$current_stage = get_post_meta( $order_id, '_gigneo_pm_activity_stage', true );
	$current_stage = ! empty( $current_stage ) ? $current_stage : 'order_placed';

	$stages = array(
		'order_placed'             => array(
			'title'       => esc_html__( 'Order placed', 'gigneo' ),
			'description' => array(
				'employer'      => esc_html__( 'Your order has been placed successfully.', 'gigneo' ),
				'freelancer'    => esc_html__( 'A new order has been placed for you.', 'gigneo' ),
				'administrator' => esc_html__( 'The order has been created and is awaiting freelancer action.', 'gigneo' ),
			),
		),
		'order_started'            => array(
			'title'       => esc_html__( 'In progress', 'gigneo' ),
			'description' => array(
				'employer'      => esc_html__( 'Working on your order has started.', 'gigneo' ),
				'freelancer'    => esc_html__( 'You have started working on this order.', 'gigneo' ),
				'administrator' => esc_html__( 'Work has begun on the order.', 'gigneo' ),
			),
		),
		'order_delivered'          => array(
			'title'       => esc_html__( 'Order delivered', 'gigneo' ),
			'description' => array(
				'employer'      => esc_html__( 'The freelancer has delivered the work. Please review it.', 'gigneo' ),
				'freelancer'    => esc_html__( 'You have delivered the work and are waiting for client review.', 'gigneo' ),
				'administrator' => esc_html__( 'The freelancer delivered the order. Waiting for employer review.', 'gigneo' ),
			),
		),
		'order_revision_requested' => array(
			'title'       => esc_html__( 'Revision requested', 'gigneo' ),
			'description' => array(
				'employer'      => esc_html__( 'You have requested a revision. The freelancer will make updates.', 'gigneo' ),
				'freelancer'    => esc_html__( 'Employer requested a revision. Please make the necessary updates.', 'gigneo' ),
				'administrator' => esc_html__( 'Employer has requested a revision from the freelancer.', 'gigneo' ),
			),
		),
		'order_revision_delivered' => array(
			'title'       => esc_html__( 'Revision delivered', 'gigneo' ),
			'description' => array(
				'employer'      => esc_html__( 'The freelancer has submitted the revised work for your review.', 'gigneo' ),
				'freelancer'    => esc_html__( 'You have submitted the revised work for review.', 'gigneo' ),
				'administrator' => esc_html__( 'The freelancer delivered the revised work. Waiting for employer review.', 'gigneo' ),
			),
		),
		'order_completed'          => array(
			'title'       => esc_html__( 'Order completed', 'gigneo' ),
			'description' => array(
				'employer'      => esc_html__( 'You have marked this order as completed. Thank you!', 'gigneo' ),
				'freelancer'    => esc_html__( 'The client has marked this order as completed. Congratulations!', 'gigneo' ),
				'administrator' => esc_html__( 'The order has been marked as completed.', 'gigneo' ),
			),
		),
		'order_cancelled'          => array(
			'title'       => esc_html__( 'Order cancelled', 'gigneo' ),
			'description' => array(
				'employer'      => esc_html__( 'This order has been cancelled.', 'gigneo' ),
				'freelancer'    => esc_html__( 'This order has been cancelled.', 'gigneo' ),
				'administrator' => esc_html__( 'This order has been cancelled.', 'gigneo' ),
			),
		),
	);

	$stages = apply_filters( 'gigneo_order_stages', $stages, $order_id, $role );

	if ( 'order_revision_requested' !== $current_stage && 'order_revision_delivered' !== $current_stage ) {
		unset( $stages['order_revision_requested'] );
	}

	if ( 'order_cancelled' === $current_stage ) {
		unset( $stages['order_completed'] );
	} else {
		unset( $stages['order_cancelled'] );
	}

	if ( 'order_revision_delivered' !== $current_stage ) {
		unset( $stages['order_revision_delivered'] );
	}

	$order_delivery_activity = gigneo_post_meta( $order_id, '_gigneo_pm_delivery_activity' );

	if ( empty( $order_delivery_activity ) ) {
		unset( $stages['order_delivered'] );
	}

	$final_stages  = array();
	$stage_keys    = array_keys( $stages );
	$current_index = array_search( $current_stage, $stage_keys, true );

	foreach ( $stage_keys as $i => $key ) {
		if ( $i < $current_index ) {
			$status = 'completed';
		} elseif ( $i === $current_index ) {
			if ( array_key_last( $stage_keys ) === $i ) {
				$status = 'completed';
			} else {
				$status = 'active';
			}
		} else {
			$status = 'pending';
		}

		$stage = $stages[ $key ];

		if ( $role && isset( $stage['description'][ $role ] ) ) {
			$stage['description'] = $stage['description'][ $role ];
		}

		$stage['status']      = $status;
		$final_stages[ $key ] = $stage;
	}

	return apply_filters( 'gigneo_order_final_stages', $final_stages, $order_id, $role );
}

/**
 * Retrieve activity messages for a given type and role.
 *
 * This function returns activity messages based on the provided type and role.
 *
 * @param string $type Optional. The type of activity message to retrieve.
 * @param string $role Optional. The role to filter messages by.
 * @return array The array of activity messages.
 * @since 0.0.1
 */
function gigneo_activity_message( $type, $role = '' ) {

	$message = '';

	if ( empty( $role ) ) {
		$role = gigneo_profile_type();
	}

	$freelancer_singular = gigneo_freelancer_singular();
	$employer_singular   = gigneo_employer_singular();

	$messages = array(
		'order_placed'                                     => array(
			'employer'      => esc_html__( 'You placed the order.', 'gigneo' ),
			'freelancer'    => esc_html__( 'Placed the order.', 'gigneo' ),
			'administrator' => esc_html__( 'Placed the order.', 'gigneo' ),
		),
		'requirement_pending'                              => array(
			'employer'      => esc_html__( 'Submit the requirements to get started.', 'gigneo' ),
			'freelancer'    => esc_html__( 'Waiting for the requirements to get started.', 'gigneo' ),
			'administrator' => esc_html__( 'Requirements are pending for this order.', 'gigneo' ),
		),
		'requirement_submitted'                            => array(
			'employer'      => esc_html__( 'You submitted the requirements.', 'gigneo' ),
			'freelancer'    => esc_html__( 'Submitted the requirements.', 'gigneo' ),
			'administrator' => esc_html__( 'Submitted the requirements.', 'gigneo' ),
		),
		'requirement_updated'                              => array(
			'employer'      => esc_html__( 'You updated the requirements.', 'gigneo' ),
			'freelancer'    => esc_html__( 'Updated the requirements.', 'gigneo' ),
			'administrator' => esc_html__( 'Updated the requirements', 'gigneo' ),
		),
		'order_started'                                    => array(
			'employer'      => esc_html__( 'The order has been started.', 'gigneo' ),
			'freelancer'    => esc_html__( 'The order has been started.', 'gigneo' ),
			'administrator' => esc_html__( 'The order has been started.', 'gigneo' ),
		),
		'order_draft_delivery'                             => array(
			'employer'      => esc_html__( 'The draft work has been delivered.', 'gigneo' ),
			'freelancer'    => esc_html__( 'You delivered the draft work.', 'gigneo' ),
			'administrator' => esc_html__( 'The draft work has been delivered.', 'gigneo' ),
		),
		'order_final_delivery'                             => array(
			'employer'      => esc_html__( 'Delivered the final work.', 'gigneo' ),
			'freelancer'    => esc_html__( 'You delivered the final work.', 'gigneo' ),
			'administrator' => esc_html__( 'Delivered the final work.', 'gigneo' ),
		),
		'order_delivered'                                  => array(
			'employer'      => esc_html__( 'The order has been delivered.', 'gigneo' ),
			'freelancer'    => esc_html__( 'You delivered the order.', 'gigneo' ),
			'administrator' => esc_html__( 'The order has been delivered.', 'gigneo' ),
		),
		'order_revision_requested'                         => array(
			'employer'      => esc_html__( 'You requested a revision.', 'gigneo' ),
			'freelancer'    => esc_html__( 'Requested a revision.', 'gigneo' ),
			'administrator' => esc_html__( 'Requested a revision.', 'gigneo' ),
		),
		'order_revision_delivered'                         => array(
			'employer'      => esc_html__( 'The revision has been delivered.', 'gigneo' ),
			'freelancer'    => esc_html__( 'You delivered the revision.', 'gigneo' ),
			'administrator' => esc_html__( 'The revision has been delivered.', 'gigneo' ),
		),
		'order_delivery_approved'                          => array(
			'employer'      => esc_html__( 'You approved the delivery.', 'gigneo' ),
			'freelancer'    => esc_html__( 'Approved the delivery.', 'gigneo' ),
			'administrator' => esc_html__( 'Approved the delivery.', 'gigneo' ),
		),
		'delivery_date_extend_request'                     => array(
			'freelancer'    => esc_html__( 'You submitted a request to extend the delivery date.', 'gigneo' ),
			'employer'      => esc_html__( 'Requested to extend the delivery date.', 'gigneo' ),
			'administrator' => esc_html__( 'Requested to extend the delivery date.', 'gigneo' ),
		),
		'delivery_date_extend_request_rejected'            => array(
			'freelancer'    => esc_html__( 'Rejected delivery date extension request.', 'gigneo' ),
			'employer'      => esc_html__( 'You rejected the delivery date extension request.', 'gigneo' ),
			'administrator' => esc_html__( 'Rejected delivery date extension request.', 'gigneo' ),
		),
		'delivery_date_extend_request_approved'            => array(
			'freelancer'    => esc_html__( 'Delivery date extension request was approved.', 'gigneo' ),
			'employer'      => esc_html__( 'You approved the delivery date extension request.', 'gigneo' ),
			'administrator' => esc_html__( 'Delivery date extension request was approved.', 'gigneo' ),
		),
		'attachment_upload'                                => array(
			'employer'      => esc_html__( 'Attachments uploaded.', 'gigneo' ),
			'freelancer'    => esc_html__( 'Attachments uploaded.', 'gigneo' ),
			'administrator' => esc_html__( 'Attachments uploaded.', 'gigneo' ),
		),
		'order_completed'                                  => array(
			'employer'      => esc_html__( 'You have completed the order.', 'gigneo' ),
			'freelancer'    => esc_html__( 'The order has been completed.', 'gigneo' ),
			'administrator' => esc_html__( 'The order has been completed.', 'gigneo' ),
		),
		'freelancer_review'                                => array(
			'employer'      => sprintf(
					// translators: %s is the freelancer singular name
				esc_html__( 'You reviewed %s.', 'gigneo' ),
				esc_html( $freelancer_singular )
			),
			'freelancer'    => esc_html__( 'Reviewed you.', 'gigneo' ),
			'administrator' => sprintf(
					// translators: %s is the freelancer singular name
				esc_html__( 'Reviewed %s.', 'gigneo' ),
				esc_html( $freelancer_singular )
			),
		),
		'employer_review'                                  => array(
			'employer'      => sprintf(
				// translators: %s is the freelancer singular name
				esc_html__( '%s reviewed you.', 'gigneo' ),
				esc_html( $freelancer_singular )
			),
			'freelancer'    => sprintf(
				// translators: %s is the employer singular name
				esc_html__( 'You reviewed the %s.', 'gigneo' ),
				esc_html( $employer_singular )
			),
			'administrator' => sprintf(
				// translators: %s is the employer singular name
				esc_html__( 'Reviewed %s.', 'gigneo' ),
				esc_html( $employer_singular )
			),
		),
		'order_payment_released'                           => array(
			'employer'      => esc_html__( 'The payment has been released.', 'gigneo' ),
			'freelancer'    => esc_html__( 'The payment has been released.', 'gigneo' ),
			'administrator' => esc_html__( 'Payment has been released for this order.', 'gigneo' ),
		),
		'order_payment_refunded'                           => array(
			'employer'      => esc_html__( 'The payment has been refunded.', 'gigneo' ),
			'freelancer'    => esc_html__( 'The payment has been refunded.', 'gigneo' ),
			'administrator' => esc_html__( 'Payment has been refunded for this order.', 'gigneo' ),
		),
		'order_cancel_requested'                           => array(
			'employer'      => esc_html__( 'You sent a cancellation request.', 'gigneo' ),
			'freelancer'    => esc_html__( 'A cancellation was requested.', 'gigneo' ),
			'administrator' => esc_html__( 'A cancellation request has been sent.', 'gigneo' ),
		),
		'order_cancel_request_rejected'                    => array(
			'employer'      => esc_html__( 'The cancellation request was rejected.', 'gigneo' ),
			'freelancer'    => esc_html__( 'You rejected the cancellation request.', 'gigneo' ),
			'administrator' => esc_html__( 'The cancellation request was rejected.', 'gigneo' ),
		),
		'order_cancel_request_approved'                    => array(
			'employer'      => esc_html__( 'The cancellation request was approved.', 'gigneo' ),
			'freelancer'    => esc_html__( 'You approved the cancellation request.', 'gigneo' ),
			'administrator' => esc_html__( 'The cancellation request was approved.', 'gigneo' ),
		),
		'order_cancelled'                                  => array(
			'employer'      => esc_html__( 'The order has been cancelled.', 'gigneo' ),
			'freelancer'    => esc_html__( 'The order has been cancelled.', 'gigneo' ),
			'administrator' => esc_html__( 'The order has been cancelled.', 'gigneo' ),
		),
		'order_cancelled_by_moderator'                     => array(
			'employer'      => esc_html__( 'Cancelled the order', 'gigneo' ),
			'freelancer'    => esc_html__( 'Cancelled the order', 'gigneo' ),
			'administrator' => esc_html__( 'Cancelled the order', 'gigneo' ),
		),
		'order_completed_by_moderator'                     => array(
			'employer'      => esc_html__( 'Completed the order.', 'gigneo' ),
			'freelancer'    => esc_html__( 'Completed the order.', 'gigneo' ),
			'administrator' => esc_html__( 'completed the order.', 'gigneo' ),
		),
		'order_disputed_by_employer'                       => array(
			'employer'      => esc_html__( 'You have disputed the order.', 'gigneo' ),
			'freelancer'    => esc_html__( 'has raised a disputed on the order.', 'gigneo' ),
			'administrator' => esc_html__( 'has raised a disputed on the order.', 'gigneo' ),
		),
		'order_disputed_by_freelancer'                     => array(
			'employer'      => esc_html__( 'has raised a disputed on the order.', 'gigneo' ),
			'freelancer'    => esc_html__( 'You have disputed the order.', 'gigneo' ),
			'administrator' => esc_html__( 'has raised a disputed on the order.', 'gigneo' ),
		),
		'order_dispute_approved_by_moderator'              => array(
			'employer'      => esc_html__( 'Approved the dispute', 'gigneo' ),
			'freelancer'    => esc_html__( 'Approved the dispute', 'gigneo' ),
			'administrator' => esc_html__( 'Approved the dispute', 'gigneo' ),
		),
		'order_dispute_rejected_by_moderator'              => array(
			'employer'      => esc_html__( 'Rejected the dispute', 'gigneo' ),
			'freelancer'    => esc_html__( 'Rejected the dispute', 'gigneo' ),
			'administrator' => esc_html__( 'Rejected the dispute', 'gigneo' ),
		),
		'order_continue_and_dispute_rejected_by_moderator' => array(
			'employer'      => esc_html__( 'Rejected the dispute and continue the order.', 'gigneo' ),
			'freelancer'    => esc_html__( 'Rejected the dispute and continue the order.', 'gigneo' ),
			'administrator' => esc_html__( 'Rejected the dispute and continue the order.', 'gigneo' ),
		),
	);

	$messages = apply_filters( 'gigneo_activity_message', $messages, $type, $role );

	if ( ! empty( $type ) && ! empty( $messages[ $type ][ $role ] ) ) {
		$message = $messages[ $type ][ $role ];
	}

	return apply_filters( 'gigneo_activity_message', $message, $type, $role );
}

/**
 * Get the icon for a given activity type.
 *
 * This function returns icon class associated with the provided activity type.
 *
 * @param string $type The activity type.
 * @return array The icon.
 * @since 0.0.1
 */
function gigneo_activity_icon( $type ) {
	$icon = array();

	$icons = array(
		'order_placed'                                     => array(
			'icon'   => 'hi hi-shopping-cart',
			'status' => 'info',
		),
		'requirement_pending'                              => array(
			'icon'   => 'hi hi-document-text',
			'status' => 'warning',
		),
		'requirement_submitted'                            => array(
			'icon'   => 'hi hi-paper-airplane',
			'status' => 'info',
		),
		'requirement_updated'                              => array(
			'icon'   => 'hi hi-pencil-square',
			'status' => 'info',
		),
		'order_started'                                    => array(
			'icon'   => 'hi hi-play-circle',
			'status' => 'warning',
		),
		'order_draft_delivery'                             => array(
			'icon'   => 'hi hi-document-duplicate',
			'status' => 'info',
		),
		'order_final_delivery'                             => array(
			'icon'   => 'hi hi-check-circle',
			'status' => 'warning',
		),
		'order_delivered'                                  => array(
			'icon'   => 'hi hi-truck',
			'status' => 'success',
		),
		'order_revision_requested'                         => array(
			'icon'   => 'hi hi-arrow-path',
			'status' => 'warning',
		),
		'order_revision_delivered'                         => array(
			'icon'   => 'hi hi-arrow-up-tray',
			'status' => 'info',
		),
		'order_delivery_approved'                          => array(
			'icon'   => 'hi hi-hand-thumb-up',
			'status' => 'success',
		),
		'delivery_date_extend_request'                     => array(
			'icon'   => 'hi hi-clock',
			'status' => 'warning',
		),
		'delivery_date_extend_request_approved'            => array(
			'icon'   => 'hi hi-calendar-days',
			'status' => 'success',
		),
		'delivery_date_extend_request_rejected'            => array(
			'icon'   => 'hi hi-calendar-days',
			'status' => 'danger',
		),
		'attachment_upload'                                => array(
			'icon'   => 'hi hi-paper-clip',
			'status' => 'info',
		),
		'order_completed'                                  => array(
			'icon'   => 'hi hi-flag',
			'status' => 'success',
		),
		'freelancer_review'                                => array(
			'icon'   => 'hi hi-star',
			'status' => 'warning',
		),
		'employer_review'                                  => array(
			'icon'   => 'hi hi-star',
			'status' => 'warning',
		),
		'order_payment_released'                           => array(
			'icon'   => 'hi hi-wallet',
			'status' => 'info',
		),
		'order_payment_refunded'                           => array(
			'icon'   => 'hi hi-wallet',
			'status' => 'danger',
		),
		'order_cancelled'                                  => array(
			'icon'   => 'hi hi-x-mark',
			'status' => 'danger',
		),
		'order_cancel_requested'                           => array(
			'icon'   => 'hi hi-exclamation-triangle',
			'status' => 'warning',
		),
		'order_cancel_request_rejected'                    => array(
			'icon'   => 'hi hi-x-circle',
			'status' => 'danger',
		),
		'order_cancel_request_approved'                    => array(
			'icon'   => 'hi hi-check-circle',
			'status' => 'success',
		),
		'order_completed_by_moderator'                     => array(
			'icon'   => 'hi hi-check-circle',
			'status' => 'success',
		),
		'order_cancelled_by_moderator'                     => array(
			'icon'   => 'hi hi-x-circle',
			'status' => 'danger',
		),

		'order_disputed_by_employer'                       => array(
			'icon'   => 'hi hi-exclamation-triangle',
			'status' => 'danger',
		),
		'order_disputed_by_freelancer'                     => array(
			'icon'   => 'hi hi-exclamation-triangle',
			'status' => 'danger',
		),
		'moderator_freelancer_dispute_complete_order'      => array(
			'icon'   => 'hi hi-flag',
			'status' => 'success',
		),
		'order_continue_and_dispute_rejected_by_moderator' => array(
			'icon'   => 'hi hi-play-circle',
			'status' => 'warning',
		),
		'order_dispute_rejected_by_moderator'              => array(
			'icon'   => 'hi hi-flag',
			'status' => 'success',
		),

	);

	$icons = apply_filters( 'gigneo_activity_icons', $icons, $type );

	if ( ! empty( $type ) && ! empty( $icons[ $type ] ) ) {
		$icon = $icons[ $type ];
	}

	return apply_filters( 'gigneo_activity_icon', $icon, $type );
}

/**
 * Add an activity entry to an order.
 *
 * This function adds a new activity to the specified order, with optional type, profile, meta, and parent activity.
 *
 * @param int $order_id The ID of the order.
 * @param string $type Optional. The type of activity.
 * @param string $profile_id Optional. The profile ID associated with the activity.
 * @param array $meta Optional. Additional meta data for the activity.
 * @param string $parent_id Optional. The parent activity ID.
 *
 * @return false|int|WP_Error
 * @since 0.0.1
 */
function gigneo_trigger_order_activity( $order_id, $type = '', $profile_id = '', $meta = array(), $parent_id = '' ) {

	if ( empty( $order_id ) ) {
		return new WP_Error( 'invalid_order_id', esc_html__( 'Something went wrong.', 'gigneo' ) );
	}

	if ( empty( $profile_id ) ) {
		$profile_id = gigneo_active_profile_id();
	}

	$current_user = wp_get_current_user();
	$user_id      = $current_user->ID;

	if ( empty( $profile_id || $user_id ) ) {
		return new WP_Error( 'invalid_profile_id', esc_html__( 'Something went wrong.', 'gigneo' ) );
	}

	$author_id = $profile_id ? get_post_field( 'post_author', $profile_id ) : $user_id;

	$comment_data = array(
		'comment_post_ID'      => absint( $order_id ),
		'user_id'              => absint( $author_id ),
		'comment_type'         => 'order_activity',
		'comment_approved'     => 1,
		'comment_author'       => '',
		'comment_author_email' => '',
		'comment_author_url'   => '',
	);

	if ( ! empty( $parent_id ) ) {
		$comment_data['comment_parent'] = $parent_id;
	}

	$comment_id = wp_insert_comment( $comment_data );

	if ( is_wp_error( $comment_id ) ) {
		return new WP_Error( 'invalid_profile_id', esc_html__( 'Something went wrong.', 'gigneo' ) );
	}

	add_comment_meta( $comment_id, '_gigneo_cm_profile_id', $profile_id, true );

	if ( ! empty( $type ) ) {
		add_comment_meta( $comment_id, '_gigneo_cm_activity_type', $type, true );
	}

	$order_type = gigneo_post_meta( $order_id, '_gigneo_pm_order_type' );
	add_comment_meta( $comment_id, '_gigneo_cm_order_type', $order_type, true );

	if ( 'service' === $order_type ) {
		$service_id = gigneo_post_meta( $order_id, '_gigneo_pm_service_id' );
		add_comment_meta( $comment_id, '_gigneo_cm_service_id', $service_id, true );
	} elseif ( 'job' === $order_type ) {
		$job_id = gigneo_post_meta( $order_id, '_gigneo_pm_job_id' );
		add_comment_meta( $comment_id, '_gigneo_cm_job_id', $job_id, true );
	}

	if ( ! empty( $meta ) && is_array( $meta ) ) {
		foreach ( $meta as $key => $value ) {
			$meta_key = sanitize_key( $key );
			add_comment_meta( $comment_id, $meta_key, $value, true );
		}
	}

	return $comment_id;
}

/**
 * Convert a UTC datetime to the profile's local timezone.
 *
 * This function converts a UTC datetime string to the local timezone of the given profile.
 *
 * @param string $datetime_utc The UTC datetime string.
 * @param string $format Optional. The output date format. Default is 'Y-m-d H:i:s'.
 * @param int|string $profile_id Optional. The profile ID to get timezone for. Default is empty.
 * @return string The converted datetime string in the local timezone.
 * @since 0.0.1
 */
function gigneo_datetime( $datetime_utc, $format = 'Y-m-d H:i:s', $profile_id = '' ) {
	if ( empty( $datetime_utc ) ) {
		return '';
	}

	try {
		$timezone = gigneo_timezone( $profile_id );

		// Convert to target timezone
		$dt = new DateTime( $datetime_utc, new DateTimeZone( 'UTC' ) );
		$dt->setTimezone( new DateTimeZone( $timezone ) );

		return wp_date(
			$format,
			$dt->getTimestamp(),
			new DateTimeZone( $timezone )
		);

	} catch ( Exception $e ) {
		return '';
	}
}

/**
 * Retrieve the timezone for a given profile.
 *
 * This function returns the timezone string associated with the provided profile ID.
 *
 * @param int|string $profile_id Optional. The profile ID to get the timezone for. Default is empty.
 * @return string The timezone string.
 * @since 0.0.1
 */
function gigneo_timezone( $profile_id = '' ) {
	$timezone = wp_timezone_string();
	return apply_filters( 'gigneo_timezone', $timezone, $profile_id );
}

/**
 * Prepare a datetime string for display in the profile's local timezone.
 *
 * Converts the given datetime to the profile's timezone and formats it.
 *
 * @param string $datetime The datetime string to convert.
 * @param string|int $profile_id Optional. The profile ID to get timezone for. Default is empty.
 * @param string $format Optional. The output date format. Default is 'Y-m-d H:i:s'.
 *
 * @return array The formatted datetime string in different timezones.
 * @since 0.0.1
 */
function gigneo_prepare_datetime( $datetime, $format = 'Y-m-d H:i:s', $profile_id = '' ) {

	if ( empty( $datetime ) ) {
		return array(
			'user'  => '',
			'local' => '',
			'gmt'   => '',
		);
	}

	if ( empty( $profile_id ) ) {
		$profile_id = gigneo_active_profile_id();
	}

	$author_id = get_post_field( 'post_author', $profile_id );

	// Determine user timezone
	$user_timezone = gigneo_timezone( $profile_id );

	try {
		$tz_user = new DateTimeZone( $user_timezone );
		$tz_wp   = wp_timezone();
		$tz_utc  = new DateTimeZone( 'UTC' );

		$dt_user = new DateTime( $datetime, $tz_user );

		$dt_wp = clone $dt_user;
		$dt_wp->setTimezone( $tz_wp );

		$dt_utc = clone $dt_user;
		$dt_utc->setTimezone( $tz_utc );

		return array(
			'user'  => $dt_user->format( $format ),
			'local' => $dt_wp->format( $format ),
			'gmt'   => $dt_utc->format( $format ),
		);

	} catch ( Exception $e ) {
		return array(
			'user'  => '',
			'local' => '',
			'gmt'   => '',
		);
	}
}

/**
 * Retrieve the browser timezone string.
 *
 * This function attempts to get the browser's timezone, typically from a cookie or request.
 *
 * @return string The detected browser timezone, or an empty string if not available.
 * @since 0.0.1
 */
function gigneo_browser_timezone() {

	if ( isset( $_COOKIE['gigneo_user_timezone'] ) ) {
		$timezone = sanitize_text_field( wp_unslash( $_COOKIE['gigneo_user_timezone'] ) );
		if ( in_array( $timezone, timezone_identifiers_list(), true ) ) {
			return $timezone;
		}
	}

	return '';
}

/**
 * Calculate average rating for a freelancer's service.
 *
 * Retrieves and returns the average rating for the service identified by the given post ID.
 *
 * @param int $post_id Service post ID.
 * @return float Average rating (0.0 if there are no ratings).
 * @since 0.0.1
 */
function gigneo_service_average_rating( $post_id ) {

	$average_rating = 0;

	if ( empty( $post_id ) ) {
		return apply_filters( 'gigneo_freelancer_average_rating', $average_rating, $post_id );
	}

	$post_type = get_post_type( $post_id );

	if ( 'gigneo_service' !== $post_type ) {
		return apply_filters( 'gigneo_freelancer_average_rating', $average_rating, $post_id );
	}

	$activity_comments = get_comments(
		array(
			'type'       => 'order_activity',
			'orderby'    => 'comment_date',
			'order'      => 'ASC',
			'parent'     => 0,
			// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
			'meta_query' => array(
				array(
					'key'     => '_gigneo_cm_service_id',
					'value'   => $post_id,
					'compare' => '=',
				),
				array(
					'key'     => '_gigneo_cm_activity_type',
					'value'   => 'freelancer_review',
					'compare' => '=',
				),
			),
			// phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		)
	);

	if ( ! empty( $activity_comments ) ) {
		$total_rating  = 0;
		$total_reviews = 0;

		foreach ( $activity_comments as $activity ) {
			$review_data         = get_comment_meta( $activity->comment_ID, '_gigneo_cm_review_info', true );
			$communication_level = ! empty( $review_data['communication_level'] ) ? intval( $review_data['communication_level'] ) : 0;
			$quality_work        = ! empty( $review_data['quality_work'] ) ? intval( $review_data['quality_work'] ) : 0;
			$understanding       = ! empty( $review_data['understanding'] ) ? intval( $review_data['understanding'] ) : 0;

			if ( $communication_level || $quality_work || $understanding ) {
				$review_total   = $communication_level + $quality_work + $understanding;
				$review_average = $review_total / 3;
				$total_rating  += $review_average;
				++$total_reviews;
			}
		}

		if ( $total_reviews > 0 ) {
			$average_rating = round( $total_rating / $total_reviews, 2 ); // final average (2 decimal places)
		}
	}

	return apply_filters( 'gigneo_service_average_rating', $average_rating, $post_id );
}

/**
 * Calculate average rating for a freelancer profile.
 *
 * Retrieves and returns the average rating for the freelancer identified by the given profile ID.
 *
 * @param int|string $profile_id Profile ID.
 *
 * @return float Average rating (0.0 if there are no ratings).
 * @since 0.0.1
 */
function gigneo_profile_rating( $profile_id, $type = 'count' ) {

	$average_rating = 0;
	$total_reviews  = 0;

	if ( empty( $profile_id ) ) {
		return apply_filters( 'gigneo_profile_rating', 'count' === $type ? $total_reviews : $average_rating, $profile_id, $type );
	}

	// Get stored ratings
	$service_order_ratings = gigneo_post_meta( $profile_id, '_gigneo_pm_service_orders_rating' );
	$job_order_ratings     = gigneo_post_meta( $profile_id, '_gigneo_pm_job_orders_rating' );

	// Ensure both are arrays
	if ( ! is_array( $service_order_ratings ) ) {
		$service_order_ratings = array();
	}
	if ( ! is_array( $job_order_ratings ) ) {
		$job_order_ratings = array();
	}

	// Merge arrays safely
	$all_ratings = array_merge( $service_order_ratings, $job_order_ratings );

	// Count of ratings
	$total_reviews = count( $all_ratings );

	// Calculate the overall average if needed
	if ( 'average' === $type && $total_reviews > 0 ) {
		$total_rating = 0;

		foreach ( $all_ratings as $rating ) {
			$total_rating += floatval( $rating );
		}

		$average_rating = round( $total_rating / $total_reviews, 1 ); // one decimal place
	}

	// Return based on requested type
	return apply_filters( 'gigneo_profile_rating', 'average' === $type ? $average_rating : $total_reviews, $profile_id, $type );
}


/**
 * Get the allowed HTML tags for the Gigneo plugin.
 *
 * Returns an array of allowed HTML tags and their permitted attributes.
 *
 * @return array The allowed HTML tags and attributes.
 * @since 0.0.1
 */
function gigneo_allowed_html_tags() {
	$allowed_tags = array(
		'h1'         => array(),
		'h2'         => array(),
		'h3'         => array(),
		'h4'         => array(),
		'h5'         => array(),
		'h6'         => array(),
		'p'          => array(),
		'br'         => array(),
		'hr'         => array(),

		'strong'     => array(),
		'b'          => array(),
		'em'         => array(),
		'i'          => array(),
		'u'          => array(),
		's'          => array(),
		'del'        => array(),
		'ins'        => array(),
		'mark'       => array(),

		'blockquote' => array(),
		'cite'       => array(),
		'code'       => array(),
		'pre'        => array(),

		'ul'         => array(),
		'ol'         => array(),
		'li'         => array(),

		'a'          => array(
			'href'   => true,
			'title'  => true,
			'target' => true,
			'rel'    => true,
		),

		'img'        => array(
			'src'    => true,
			'alt'    => true,
			'width'  => true,
			'height' => true,
		),

		/* ======================
		 * SVG SUPPORT
		 * ====================== */
		'svg'        => array(
			'xmlns'       => true,
			'viewBox'     => true,
			'fill'        => true,
			'width'       => true,
			'height'      => true,
			'class'       => true,
			'aria-hidden' => true,
			'role'        => true,
		),

		'g'          => array(
			'mask'   => true,
			'fill'   => true,
			'stroke' => true,
		),

		'path'       => array(
			'd'                => true,
			'fill'             => true,
			'stroke'           => true,
			'stroke-width'     => true,
			'stroke-linecap'   => true,
			'stroke-linejoin'  => true,
			'stroke-dasharray' => true,
		),

		'circle'     => array(
			'cx'   => true,
			'cy'   => true,
			'r'    => true,
			'fill' => true,
		),

		'ellipse'    => array(
			'cx'        => true,
			'cy'        => true,
			'rx'        => true,
			'ry'        => true,
			'fill'      => true,
			'transform' => true,
		),

		'mask'       => array(
			'id'        => true,
			'x'         => true,
			'y'         => true,
			'width'     => true,
			'height'    => true,
			'maskUnits' => true,
			'style'     => true,
		),

		'defs'       => array(),
	);

	return apply_filters( 'gigneo_allowed_html_tags', $allowed_tags );
}

/**
 * Return a standardized "not found" response for the given type.
 *
 * @param string $type The type of resource that was not found.
 * @param array  $args Optional. Additional context or arguments.
 * @return array|WP_Error Array describing the not found response or WP_Error on failure.
 * @since 0.0.1
 */
function gigneo_not_found( $type, $args = array() ) {

	$args['type'] = $type;

	$args = apply_filters( 'gigneo_not_found', $args );

	gigneo_load_template( 'dashboard/not-found', false, $args );
}

/**
 * Render pagination for a WP_Query instance.
 *
 * @param WP_Query $query          The WP_Query instance.
 * @param int      $paged          Current page number.
 * @param string   $wrapper_class  Additional CSS class for the wrapper.
 */
function gigneo_pagination( $query, $paged = 1, $wrapper_class = '' ) {

	if ( ! $query instanceof WP_Query || $query->max_num_pages <= 1 ) {
		return;
	}

	$next_icon = '<i class="hi hi-chevron-right"></i>';
	$prev_icon = '<i class="hi hi-chevron-left"></i>';

	// Pagination links
	$pagination_links = paginate_links(
		array(
			'base'      => esc_url_raw( add_query_arg( 'step', '%#%' ) ),
			'format'    => '',
			'current'   => max( 1, (int) $paged ),
			'total'     => $query->max_num_pages,
			'prev_text' => sprintf(
				// translators: %s is previous page icon
				esc_html__( '%s Previous ', 'gigneo' ),
				wp_kses_post( $prev_icon )
			),
			'next_text' => sprintf(
				// translators: %s is next page icon
				esc_html__( 'Next %s', 'gigneo' ),
				wp_kses_post( $next_icon )
			),
			'type'      => 'array',
		)
	);

	if ( empty( $pagination_links ) ) {
		return;
	}

	$wrapper_classes  = 'gn-pagination-wrap';
	$wrapper_classes .= ! empty( $wrapper_class ) ? ' ' . esc_attr( $wrapper_class ) : '';
	?>

	<div class="<?php echo esc_attr( $wrapper_classes ); ?>">
		<ul class="gn-pagination">
			<?php foreach ( $pagination_links as $link ) : ?>
				<?php
				$link = str_replace(
					array( 'page-numbers', 'current', 'next', 'prev' ),
					array( 'gn-page-numbers', 'gn-current', 'gn-next', 'gn-prev' ),
					$link
				);
				?>
				<li><?php echo do_shortcode( $link ); ?></li>
			<?php endforeach; ?>
		</ul>
	</div>
	<?php
}

/**
 * Get profile statistics for a given type and profile ID.
 *
 * This function retrieves various statistics (lifetime, last 30 days, previous 30 days)
 * for the specified type and profile ID, and calculates differences and trends.
 *
 * @param string     $type       The type of statistic to retrieve (e.g., 'wallet_balance', 'total_earning', 'total_spending', 'available_credits').
 * @param int|string $profile_id The profile ID to retrieve statistics for.
 * @return array An associative array containing the statistics.
 * @since 0.0.1
 */
function gigneo_profile_stat( $type, $profile_id = 0 ) {
	if ( empty( $type ) || empty( $profile_id ) ) {
		return array(
			'lifetime'   => 0,
			'last_30'    => 0,
			'prev_30'    => 0,
			'difference' => 0,
			'percentage' => 0,
			'trend'      => 'neutral',
		);
	}

	global $wpdb;

	// Timezone & UTC conversion
	$tz     = new DateTimeZone( gigneo_timezone( $profile_id ) );
	$now    = new DateTime( 'now', $tz );
	$to_utc = fn( $d ) => ( clone $d )->setTimezone( new DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' );

	$last_from = $to_utc( ( clone $now )->modify( '-30 days' ) );
	$prev_from = $to_utc( ( clone $now )->modify( '-60 days' ) );
	$prev_to   = $to_utc( ( clone $now )->modify( '-30 days' ) );

	$profile_type = gigneo_profile_type( $profile_id );
	$key          = 'freelancer' === $profile_type ? '_gigneo_pm_freelancer_profile_id' : '_gigneo_pm_profile_id';

	$row = null;

	switch ( $type ) {
		case 'wallet_balance':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_WALLET'
SELECT 
                        (IFNULL(inv.last_30,0) - IFNULL(wdr.last_30,0)) AS last_30,
                        (IFNULL(inv.prev_30,0) - IFNULL(wdr.prev_30,0)) AS prev_30,
                        (IFNULL(inv.lifetime,0) - IFNULL(wdr.lifetime,0)) AS lifetime
                    FROM (
                        SELECT 
                            SUM(CASE WHEN p.post_date_gmt >= %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) END) AS last_30,
                            SUM(CASE WHEN p.post_date_gmt BETWEEN %s AND %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) END) AS prev_30,
                            SUM(CAST(pm.meta_value AS DECIMAL(12,2))) AS lifetime
                        FROM %i p
                        INNER JOIN %i pm ON p.ID = pm.post_id AND pm.meta_key = '_gigneo_pm_total'
                        INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = '_gigneo_pm_profile_id' AND ppm.meta_value = %d
                        WHERE p.post_type = 'gigneo_invoice' 
                          AND p.post_status IN ('completed','paid','refunded','withdrawn')
                    ) AS inv,
                    (
                        SELECT 
                            SUM(CASE WHEN p.post_date_gmt >= %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) END) AS last_30,
                            SUM(CASE WHEN p.post_date_gmt BETWEEN %s AND %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) END) AS prev_30,
                            SUM(CAST(pm.meta_value AS DECIMAL(12,2))) AS lifetime
                        FROM %i p
                        INNER JOIN %i pm ON p.ID = pm.post_id AND pm.meta_key = '_gigneo_pm_total'
                        INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = '_gigneo_pm_profile_id' AND ppm.meta_value = %d
                        WHERE p.post_type = 'gigneo_withdrawal' 
                          AND p.post_status IN ('publish','in-progress','on-hold')
                    ) AS wdr
GNEO_STAT_WALLET
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$profile_id,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Total Earning
		case 'total_earning':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_TOTAL_EARNING'
SELECT 
                        SUM(CASE WHEN p.post_date_gmt >= %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS last_30,
                        SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS prev_30,
                        SUM(CAST(pm.meta_value AS DECIMAL(12,2))) AS lifetime
                    FROM %i p
                    INNER JOIN %i pm ON p.ID = pm.post_id AND pm.meta_key = '_gigneo_pm_total'
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = '_gigneo_pm_profile_id' AND ppm.meta_value = %d
                    INNER JOIN %i ppt ON p.ID = ppt.post_id AND ppt.meta_key = '_gigneo_pm_type' AND ppt.meta_value IN ('service_order','job_order')
                    WHERE p.post_type = 'gigneo_invoice' 
                      AND p.post_status = 'completed'
                      AND CAST(pm.meta_value AS DECIMAL(12,2)) > 0
GNEO_STAT_TOTAL_EARNING
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$profile_id,
					$wpdb->postmeta
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Total Spending
		case 'total_spending':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_TOTAL_SPENDING'
SELECT 
                ABS(SUM(CASE WHEN p.post_date_gmt >= %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END)) AS last_30,
                ABS(SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END)) AS prev_30,
                ABS(SUM(CAST(pm.meta_value AS DECIMAL(12,2)))) AS lifetime

            FROM %i p INNER JOIN %i pm ON p.ID = pm.post_id AND pm.meta_key = '_gigneo_pm_total'
            INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = '_gigneo_pm_profile_id' AND ppm.meta_value = %d
            WHERE p.post_type = 'gigneo_invoice' 
              AND p.post_status IN ('paid','refunded')
GNEO_STAT_TOTAL_SPENDING
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Available Credits
		case 'available_credits':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_AVAILABLE_CREDITS'
SELECT 
                        SUM(CASE WHEN p.post_date_gmt >= %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS last_30,
                        SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS prev_30,
                        SUM(CAST(pm.meta_value AS DECIMAL(12,2))) AS lifetime
                    FROM %i p
                    INNER JOIN %i pm ON p.ID = pm.post_id AND pm.meta_key = '_gigneo_pm_credit'
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = '_gigneo_pm_profile_id' AND ppm.meta_value = %d
                    INNER JOIN %i ppt ON p.ID = ppt.post_id AND ppt.meta_key = '_gigneo_pm_type' AND ppt.meta_value IN ('credit','bulk_credit','monthly_credit','credit_plan','service_creation','job_creation', 'proposal_submission')
                    WHERE p.post_type = 'gigneo_invoice' 
                    AND p.post_status IN ('completed','paid')
GNEO_STAT_AVAILABLE_CREDITS
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$profile_id,
					$wpdb->postmeta
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Purchased Credits
		case 'purchased_credits':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_PURCHASED_CREDITS'
SELECT 
                        SUM(CASE WHEN p.post_date_gmt >= %s THEN CAST(pm_credit.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS last_30,
                        SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN CAST(pm_credit.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS prev_30,
                        SUM(CAST(pm_credit.meta_value AS DECIMAL(12,2))) AS lifetime
                    FROM %i p
                    INNER JOIN %i pm_total ON p.ID = pm_total.post_id AND pm_total.meta_key = '_gigneo_pm_total' AND pm_total.meta_value != ''
                    INNER JOIN %i pm_credit ON p.ID = pm_credit.post_id AND pm_credit.meta_key = '_gigneo_pm_credit'
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = '_gigneo_pm_profile_id' AND ppm.meta_value = %d
                    INNER JOIN %i ppt ON p.ID = ppt.post_id AND ppt.meta_key = '_gigneo_pm_type' AND ppt.meta_value IN ('credit','bulk_credit','monthly_credit','credit_plan','service_creation','job_creation','proposal_submission')
                    WHERE p.post_type = 'gigneo_invoice' 
                    AND p.post_status = 'paid'
GNEO_STAT_PURCHASED_CREDITS
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$profile_id,
					$wpdb->postmeta
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Used Credits
		case 'used_credits':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_USED_CREDITS'
SELECT 
                        SUM(CASE WHEN p.post_date_gmt >= %s THEN ABS(CAST(pm.meta_value AS DECIMAL(12,2))) ELSE 0 END) AS last_30,
                        SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN ABS(CAST(pm.meta_value AS DECIMAL(12,2))) ELSE 0 END) AS prev_30,
                        SUM(ABS(CAST(pm.meta_value AS DECIMAL(12,2)))) AS lifetime
                    FROM %i p
                    INNER JOIN %i pm ON p.ID = pm.post_id AND pm.meta_key = '_gigneo_pm_credit'
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = '_gigneo_pm_profile_id' AND ppm.meta_value = %d
                    INNER JOIN %i ppt ON p.ID = ppt.post_id AND ppt.meta_key = '_gigneo_pm_type' AND ppt.meta_value IN ('credit','bulk_credit','monthly_credit','credit_plan' ,'service_creation','job_creation','proposal_submission')
                    WHERE p.post_type = 'gigneo_invoice' 
                      AND p.post_status IN ('completed', 'paid') 
                      AND CAST(pm.meta_value AS DECIMAL(12,2)) < 0
GNEO_STAT_USED_CREDITS
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$profile_id,
					$wpdb->postmeta
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Total Withdrawals
		case 'total_withdrawals':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_TOTAL_WITHDRAWALS'
SELECT 
                SUM(CASE WHEN p.post_date_gmt >= %s THEN ABS(CAST(pm.meta_value AS DECIMAL(12,2))) ELSE 0 END) AS last_30,
                SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN ABS(CAST(pm.meta_value AS DECIMAL(12,2))) ELSE 0 END) AS prev_30,
                SUM(ABS(CAST(pm.meta_value AS DECIMAL(12,2)))) AS lifetime
            FROM %i p
            INNER JOIN %i pm ON p.ID = pm.post_id AND pm.meta_key = '_gigneo_pm_total'
            INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = '_gigneo_pm_profile_id' AND ppm.meta_value = %d
            INNER JOIN %i pmt ON p.ID = pmt.post_id AND pmt.meta_key = '_gigneo_pm_type' AND pmt.meta_value = 'withdraw'
            WHERE p.post_type = 'gigneo_invoice' 
              AND p.post_status ='withdrawn'
GNEO_STAT_TOTAL_WITHDRAWALS
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$profile_id,
					$wpdb->postmeta
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Requested Withdrawals
		case 'requested_withdrawals':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_REQUESTED_WITHDRAWALS'
SELECT 
                        SUM(CASE WHEN p.post_date_gmt >= %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS last_30,
                        SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS prev_30,
                        SUM(CAST(pm.meta_value AS DECIMAL(12,2))) AS lifetime
                    FROM %i p
                    INNER JOIN %i pm ON p.ID = pm.post_id AND pm.meta_key = '_gigneo_pm_total'
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = '_gigneo_pm_profile_id' AND ppm.meta_value = %d
                    WHERE p.post_type = 'gigneo_withdrawal' 
                      AND p.post_status IN ('publish', 'in-progress', 'on-hold')
GNEO_STAT_REQUESTED_WITHDRAWALS
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Order Total
		case 'total_orders_total':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_ORDER_TOTAL_T'
SELECT 
                        SUM(CASE WHEN p.post_date_gmt >= %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS last_30,
                        SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS prev_30,
                        SUM(CAST(pm.meta_value AS DECIMAL(12,2))) AS lifetime
                    FROM %i p
                    INNER JOIN %i pm ON p.ID = pm.post_id AND pm.meta_key = '_gigneo_pm_total'
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = %s AND ppm.meta_value = %d
                    WHERE p.post_type = 'gigneo_order' 
                      AND p.post_status IN ('publish','delivered','disputed','completed','cancelled')
GNEO_STAT_ORDER_TOTAL_T
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$key,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		case 'completed_orders_total':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_ORDER_COMPL_T'
SELECT 
                        SUM(CASE WHEN p.post_date_gmt >= %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS last_30,
                        SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS prev_30,
                        SUM(CAST(pm.meta_value AS DECIMAL(12,2))) AS lifetime
                    FROM %i p
                    INNER JOIN %i pm ON p.ID = pm.post_id AND pm.meta_key = '_gigneo_pm_total'
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = %s AND ppm.meta_value = %d
                    WHERE p.post_type = 'gigneo_order' 
                      AND p.post_status IN ('completed')
GNEO_STAT_ORDER_COMPL_T
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$key,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		case 'active_orders_total':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_ORDER_ACT_T'
SELECT 
                        SUM(CASE WHEN p.post_date_gmt >= %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS last_30,
                        SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS prev_30,
                        SUM(CAST(pm.meta_value AS DECIMAL(12,2))) AS lifetime
                    FROM %i p
                    INNER JOIN %i pm ON p.ID = pm.post_id AND pm.meta_key = '_gigneo_pm_total'
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = %s AND ppm.meta_value = %d
                    WHERE p.post_type = 'gigneo_order' 
                      AND p.post_status IN ('publish','delivered','disputed')
GNEO_STAT_ORDER_ACT_T
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$key,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		case 'cancelled_orders_total':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_ORDER_CAN_T'
SELECT 
                        SUM(CASE WHEN p.post_date_gmt >= %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS last_30,
                        SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN CAST(pm.meta_value AS DECIMAL(12,2)) ELSE 0 END) AS prev_30,
                        SUM(CAST(pm.meta_value AS DECIMAL(12,2))) AS lifetime
                    FROM %i p
                    INNER JOIN %i pm ON p.ID = pm.post_id AND pm.meta_key = '_gigneo_pm_total'
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = %s AND ppm.meta_value = %d
                    WHERE p.post_type = 'gigneo_order' 
                      AND p.post_status IN ('cancelled')
GNEO_STAT_ORDER_CAN_T
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$wpdb->postmeta,
					$key,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Order Count
		case 'total_orders_count':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_ORDER_TOTAL_C'
SELECT 
                        COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                        COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                        COUNT(*) AS lifetime
                    FROM %i p
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = %s AND ppm.meta_value = %d
                    WHERE p.post_type = 'gigneo_order' 
                      AND p.post_status IN ('publish','delivered','disputed','completed','cancelled')
GNEO_STAT_ORDER_TOTAL_C
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$key,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		case 'completed_orders_count':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_ORDER_COMPL_C'
SELECT 
                        COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                        COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                        COUNT(*) AS lifetime
                    FROM %i p
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = %s AND ppm.meta_value = %d
                    WHERE p.post_type = 'gigneo_order' 
                      AND p.post_status IN ('completed')
GNEO_STAT_ORDER_COMPL_C
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$key,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		case 'active_orders_count':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_ORDER_ACT_C'
SELECT 
                        COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                        COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                        COUNT(*) AS lifetime
                    FROM %i p
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = %s AND ppm.meta_value = %d
                    WHERE p.post_type = 'gigneo_order' 
                      AND p.post_status IN ('publish','delivered','disputed')
GNEO_STAT_ORDER_ACT_C
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$key,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		case 'cancelled_orders_count':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_ORDER_CAN_C'
SELECT 
                        COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                        COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                        COUNT(*) AS lifetime
                    FROM %i p
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = %s AND ppm.meta_value = %d
                    WHERE p.post_type = 'gigneo_order' 
                      AND p.post_status IN ('cancelled')
GNEO_STAT_ORDER_CAN_C
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$key,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Service Count
		case 'total_services_count':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_SVC_TOTAL'
SELECT 
                COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                COUNT(*) AS lifetime 
            FROM %i p
            WHERE p.post_type = 'gigneo_service'
              AND p.post_author = %d
              AND p.post_status IN ('publish','draft','rejected','paused')
GNEO_STAT_SVC_TOTAL
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					get_post_field( 'post_author', $profile_id )
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		case 'active_services_count':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_SVC_ACTIVE'
SELECT 
                COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                COUNT(*) AS lifetime 
            FROM %i p
            WHERE p.post_type = 'gigneo_service'
              AND p.post_author = %d
              AND p.post_status IN ('publish')
GNEO_STAT_SVC_ACTIVE
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					get_post_field( 'post_author', $profile_id )
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		//Jobs Count
		case 'total_job_count':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_JOB_TOTAL'
SELECT 
                COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                COUNT(*) AS lifetime 
            FROM %i p
            WHERE p.post_type = 'gigneo_job'
              AND p.post_author = %d
              AND p.post_status IN ('publish','draft','rejected','paused','completed','in-progress','pending','dispute','delivered')
GNEO_STAT_JOB_TOTAL
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					get_post_field( 'post_author', $profile_id )
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		case 'active_jobs_count':
		case 'active_job_count':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_JOB_ACTIVE'
SELECT 
                COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                COUNT(*) AS lifetime 
            FROM %i p
            WHERE p.post_type = 'gigneo_job'
              AND p.post_author = %d
              AND p.post_status IN ('publish')
GNEO_STAT_JOB_ACTIVE
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					get_post_field( 'post_author', $profile_id )
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		case 'hires_job_count':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_JOB_HIRES'
SELECT 
                COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                COUNT(*) AS lifetime 
            FROM %i p
            WHERE p.post_type = 'gigneo_job'
              AND p.post_author = %d
              AND p.post_status IN ('in-progress','completed','delivered','dispute')
GNEO_STAT_JOB_HIRES
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					get_post_field( 'post_author', $profile_id )
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		case 'completed_job_count':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_JOB_COMPL'
SELECT 
                COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                COUNT(*) AS lifetime 
            FROM %i p
            WHERE p.post_type = 'gigneo_job'
              AND p.post_author = %d
              AND p.post_status IN ('completed')
GNEO_STAT_JOB_COMPL
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					get_post_field( 'post_author', $profile_id )
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		case 'cancelled_job_count':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_JOB_CAN'
SELECT 
                COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                COUNT(*) AS lifetime 
            FROM %i p
            WHERE p.post_type = 'gigneo_job'
              AND p.post_author = %d
              AND p.post_status IN ('cancelled')
GNEO_STAT_JOB_CAN
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					get_post_field( 'post_author', $profile_id )
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Proposal Submitted
		case 'proposals_submitted':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_PROPOSALS'
SELECT 
                        COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                        COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                        COUNT(*) AS lifetime
                    FROM %i p
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = %s AND ppm.meta_value = %d
                    WHERE p.post_type = 'gigneo_proposal' 
                      AND p.post_status IN ('publish', 'hired')
GNEO_STAT_PROPOSALS
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$key,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Service Hired
		case 'services_hired':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_SVC_HIRED'
SELECT 
                        COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                        COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                        COUNT(*) AS lifetime
                    FROM %i p
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = %s AND ppm.meta_value = %d
                    INNER JOIN %i pot ON p.ID = pot.post_id AND pot.meta_key = '_gigneo_pm_order_type' AND pot.meta_value = 'service'
                    WHERE p.post_type = 'gigneo_order' 
                      AND p.post_status = 'publish'
GNEO_STAT_SVC_HIRED
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$key,
					$profile_id,
					$wpdb->postmeta
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Jobs Completed
		case 'jobs_completed':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_JOBS_COMPL'
SELECT 
                        COUNT(CASE WHEN p.post_date_gmt >= %s THEN 1 END) AS last_30,
                        COUNT(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 END) AS prev_30,
                        COUNT(*) AS lifetime
                    FROM %i p
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = %s AND ppm.meta_value = %d
                    INNER JOIN %i pot ON p.ID = pot.post_id AND pot.meta_key = '_gigneo_pm_order_type' AND pot.meta_value = 'job'
                    WHERE p.post_type = 'gigneo_order' 
                      AND p.post_status = 'completed'
GNEO_STAT_JOBS_COMPL
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$key,
					$profile_id,
					$wpdb->postmeta
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Total Reviews Submitted/Received
		case 'total_reviews':
			$review_key = 'freelancer' === $profile_type ? '_gigneo_cm_freelancer_profile_id' : '_gigneo_cm_profile_id';
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_TOTAL_REVIEWS'
SELECT 
                        SUM(CASE WHEN c.comment_date_gmt >= %s THEN 1 ELSE 0 END) AS last_30,
                        SUM(CASE WHEN c.comment_date_gmt >= %s AND c.comment_date_gmt < %s THEN 1 ELSE 0 END) AS prev_30,
                        COUNT(*) AS lifetime
                    FROM %i c
                    INNER JOIN %i cm_activity ON c.comment_ID = cm_activity.comment_id AND cm_activity.meta_key = '_gigneo_cm_activity_type' AND cm_activity.meta_value = 'freelancer_review'
                    INNER JOIN %i cm_profile ON c.comment_ID = cm_profile.comment_id AND cm_profile.meta_key = %s AND cm_profile.meta_value = %d
GNEO_STAT_TOTAL_REVIEWS
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->comments,
					$wpdb->commentmeta,
					$wpdb->commentmeta,
					$review_key,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Average Rating
		case 'average_rating':
			$review_key = 'freelancer' === $profile_type ? '_gigneo_cm_freelancer_profile_id' : '_gigneo_cm_profile_id';
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_AVG_RATING'
SELECT 
                        AVG(CASE WHEN c.comment_date_gmt >= %s THEN CAST(cm_rating.meta_value AS DECIMAL(3,2)) END) AS last_30,
                        AVG(CASE WHEN c.comment_date_gmt >= %s AND c.comment_date_gmt < %s THEN CAST(cm_rating.meta_value AS DECIMAL(3,2)) END) AS prev_30,
                        AVG(CAST(cm_rating.meta_value AS DECIMAL(3,2))) AS lifetime
                    FROM %i c
                    INNER JOIN %i cm_activity ON c.comment_ID = cm_activity.comment_id AND cm_activity.meta_key = '_gigneo_cm_activity_type' AND cm_activity.meta_value = 'freelancer_review'
                    INNER JOIN %i cm_profile ON c.comment_ID = cm_profile.comment_id AND cm_profile.meta_key = %s AND cm_profile.meta_value = %d
                    INNER JOIN %i cm_rating ON c.comment_ID = cm_rating.comment_id AND cm_rating.meta_key = '_gigneo_cm_average_rating'
                    WHERE c.comment_type = 'order_activity'
GNEO_STAT_AVG_RATING
							,
					$last_from,
					$prev_from,
					$prev_to,
					$wpdb->comments,
					$wpdb->commentmeta,
					$wpdb->commentmeta,
					$review_key,
					$profile_id,
					$wpdb->commentmeta
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Success Rate
		case 'success_rate':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_SUCCESS_RATE'
SELECT 
                      ROUND((SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_status IN ('completed', 'publish', 'disputed') THEN 1 ELSE 0 END) * 100.0) / NULLIF(SUM(CASE WHEN p.post_date_gmt >= %s THEN 1 ELSE 0 END), 0), 2 ) AS last_30,
                      ROUND((SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s AND p.post_status IN ('completed', 'publish', 'disputed') THEN 1 ELSE 0 END) * 100.0) / NULLIF(SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 ELSE 0 END), 0), 2 ) AS prev_30,
                      ROUND((COUNT(CASE WHEN p.post_status IN ('completed', 'publish', 'disputed') THEN 1 END) * 100.0) / NULLIF(COUNT(*), 0), 2 ) AS lifetime
                  FROM %i p
                  INNER JOIN %i ppm ON p.ID = ppm.post_id 
                       AND ppm.meta_key = %s 
                       AND ppm.meta_value = %d
                  WHERE p.post_type = 'gigneo_order' 
                    AND p.post_status IN ('publish', 'completed', 'cancelled', 'disputed')
GNEO_STAT_SUCCESS_RATE
							,
					$last_from,
					$last_from,
					$prev_from,
					$prev_to,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$key,
					$profile_id
				),
				ARRAY_A
			);
            // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		// Cancellation Rate
		case 'cancellation_rate':
            // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$row = $wpdb->get_row(
				$wpdb->prepare(
					<<<'GNEO_STAT_CANCEL_RATE'
                            SELECT
                        ROUND((SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_status = 'cancelled' THEN 1 ELSE 0 END) * 100.0) / NULLIF(SUM(CASE WHEN p.post_date_gmt >= %s THEN 1 ELSE 0 END), 0), 2 ) AS last_30,
                        ROUND((SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s AND p.post_status = 'cancelled' THEN 1 ELSE 0 END) * 100.0) / NULLIF(SUM(CASE WHEN p.post_date_gmt >= %s AND p.post_date_gmt < %s THEN 1 ELSE 0 END), 0), 2 ) AS prev_30,
                        ROUND((COUNT(CASE WHEN p.post_status = 'cancelled' THEN 1 END) * 100.0) / NULLIF(COUNT(*), 0), 2 ) AS lifetime
                    FROM %i p
                    INNER JOIN %i ppm ON p.ID = ppm.post_id AND ppm.meta_key = %s AND ppm.meta_value = %d
                    WHERE p.post_type = 'gigneo_order'
        AND p.post_status IN ('publish','completed','cancelled','disputed')
GNEO_STAT_CANCEL_RATE
						,
					$last_from,
					$last_from,
					$prev_from,
					$prev_to,
					$prev_from,
					$prev_to,
					$wpdb->posts,
					$wpdb->postmeta,
					$key,
					$profile_id
				),
				ARRAY_A
			);
          // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			break;

		default:
			return array(
				'lifetime'   => 0,
				'last_30'    => 0,
				'prev_30'    => 0,
				'difference' => 0,
				'percentage' => 0,
				'trend'      => 'neutral',
			);
	}

	if ( ! is_array( $row ) ) {
		return array(
			'lifetime'   => 0,
			'last_30'    => 0,
			'prev_30'    => 0,
			'difference' => 0,
			'percentage' => 0,
			'trend'      => 'neutral',
		);
	}

	$last     = floatval( $row['last_30'] ?? 0 );
	$prev     = floatval( $row['prev_30'] ?? 0 );
	$lifetime = floatval( $row['lifetime'] ?? 0 );
	$diff     = $last - $prev;

	// Use abs() for zero check: 0 !== 0.0 is true in PHP, which wrongly skipped the else branch and caused division by zero.
	if ( abs( $prev ) > PHP_FLOAT_EPSILON ) {
		$pct = round( $diff / abs( $prev ) * 100, 2 );
	} else {
		$pct = $last > 0 ? 100 : ( $last < 0 ? -100 : 0 );
	}

	$trend = $pct < 0 ? 'down' : 'up';

	$data = array(
		'last_30'    => $last,
		'prev_30'    => $prev,
		'lifetime'   => $lifetime,
		'difference' => $diff,
		'percentage' => $pct,
		'trend'      => $trend,
	);

	return apply_filters( "gigneo_profile_stat_{$type}", $data, $profile_id );
}

function gigneo_service_orders_count( $service_id, $status = null ) {
	global $wpdb;

	if ( is_null( $status ) ) {
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$count = (int) $wpdb->get_var(
			$wpdb->prepare(
				'
        SELECT COUNT(*)
        FROM %i p
        INNER JOIN %i pm
            ON p.ID = pm.post_id
        WHERE p.post_type = %s
          AND pm.meta_key = %s
          AND pm.meta_value = %d
    ',
				$wpdb->posts,
				$wpdb->postmeta,
				'gigneo_order',
				'_gigneo_pm_service_id',
				$service_id
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		return apply_filters( 'gigneo_orders_count', $count, $service_id, $status );
	}

	$status_list = is_array( $status ) ? $status : array( $status );
	$total       = 0;
	foreach ( $status_list as $st ) {
		$st = sanitize_key( (string) $st );
		if ( '' === $st ) {
			continue;
		}
		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$total += (int) $wpdb->get_var(
			$wpdb->prepare(
				'
        SELECT COUNT(*)
        FROM %i p
        INNER JOIN %i pm
            ON p.ID = pm.post_id
        WHERE p.post_type = %s
          AND p.post_status = %s
          AND pm.meta_key = %s
          AND pm.meta_value = %d
    ',
				$wpdb->posts,
				$wpdb->postmeta,
				'gigneo_order',
				$st,
				'_gigneo_pm_service_id',
				$service_id
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	return apply_filters( 'gigneo_orders_count', $total, $service_id, $status );
}

/**
 * @throws Exception
 */
function gigneo_calculate_tax( $profile_id, $amount ) {

	$total_tax = 0;

	$withdrawal_product_id = gigneo_option( '_gigneo_ts_withdrawal_product' );
	$withdrawal_product    = ! empty( $withdrawal_product_id ) ? wc_get_product( $withdrawal_product_id ) : false;
	$post_author_id        = get_post_field( 'post_author', $profile_id );

	if ( empty( $profile_id ) || empty( $post_author_id ) || empty( $amount ) || ! $withdrawal_product ) {
		return apply_filters( 'gigneo_calculate_tax', $total_tax, $profile_id, $amount );
	}

	$customer = new WC_Customer( $post_author_id );

	$country  = $customer->get_billing_country() ?? '';
	$state    = $customer->get_billing_state() ?? '';
	$postcode = $customer->get_billing_postcode() ?? '';
	$city     = $customer->get_billing_city() ?? '';

	$tax_rates = WC_Tax::find_rates(
		array(
			'country'   => $country,
			'state'     => $state,
			'postcode'  => $postcode,
			'city'      => $city,
			'tax_class' => $withdrawal_product->get_tax_class(),
		)
	);

	$taxes     = WC_Tax::calc_tax( $amount, $tax_rates );
	$total_tax = array_sum( $taxes ) ?? 0;

	return apply_filters( 'gigneo_calculate_tax', $total_tax, $profile_id, $amount );
}

	/**
	 * Count job proposals.
	 *
	 * @param int         $job_id Job ID.
	 * @param string|array $status Optional. Proposal status or array of statuses.
	 * @return int Proposal count.
	 */
function gigneo_count_job_proposals( $job_id, $status = '' ) {

	$job_id = absint( $job_id );

	if ( ! $job_id ) {
		return 0;
	}

	global $wpdb;

	if ( ! empty( $status ) ) {
		if ( is_array( $status ) ) {
			$count = 0;
			foreach ( $status as $st ) {
				$st = sanitize_key( (string) $st );
				if ( '' === $st ) {
					continue;
				}
				// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$count += (int) $wpdb->get_var(
					$wpdb->prepare(
						'
			SELECT COUNT( p.ID )
			FROM %i AS p
			INNER JOIN %i AS pm
				ON p.ID = pm.post_id
			WHERE p.post_type = %s
				AND p.post_status = %s
				AND pm.meta_key = %s
				AND pm.meta_value = %d
		',
						$wpdb->posts,
						$wpdb->postmeta,
						'gigneo_proposal',
						$st,
						'_gigneo_pm_job_id',
						$job_id
					)
				);
				// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			}

			return $count;
		}

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		return (int) $wpdb->get_var(
			$wpdb->prepare(
				'
			SELECT COUNT( p.ID )
			FROM %i AS p
			INNER JOIN %i AS pm
				ON p.ID = pm.post_id
			WHERE p.post_type = %s
				AND p.post_status = %s
				AND pm.meta_key = %s
				AND pm.meta_value = %d
		',
				$wpdb->posts,
				$wpdb->postmeta,
				'gigneo_proposal',
				sanitize_key( (string) $status ),
				'_gigneo_pm_job_id',
				$job_id
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	}

	// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	return (int) $wpdb->get_var(
		$wpdb->prepare(
			"
			SELECT COUNT( p.ID )
			FROM %i AS p
			INNER JOIN %i AS pm
				ON p.ID = pm.post_id
			WHERE p.post_type = %s
				AND pm.meta_key = %s
				AND pm.meta_value = %d
				AND p.post_status NOT IN ( 'trash', 'auto-draft' )
		",
			$wpdb->posts,
			$wpdb->postmeta,
			'gigneo_proposal',
			'_gigneo_pm_job_id',
			$job_id
		)
	);
	// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
}
/**
	 * Count viewed proposals for a job.
	 *
	 * @param int $job_id Job ID.
	 * @return int
	 */
function gigneo_count_job_viewed( $job_id ) {

	$job_id = absint( $job_id );

	if ( ! $job_id ) {
		return 0;
	}

	global $wpdb;

	// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	return (int) $wpdb->get_var(
		$wpdb->prepare(
			'
			SELECT COUNT( p.ID )
			FROM %i AS p
			INNER JOIN %i AS pm
				ON p.ID = pm.post_id
			WHERE p.post_type = %s
				AND p.post_status = %s
				AND pm.meta_key = %s
				AND pm.meta_value = %d
		',
			$wpdb->posts,
			$wpdb->postmeta,
			'gigneo_proposal',
			'viewed',
			'_gigneo_pm_job_id',
			$job_id
		)
	);
	// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
}

/**
	 * Get last activity datetime for a job based on its proposals.
	 *
	 * @param int $job_id Job ID.
	 * @return string Datetime in MySQL format (Y-m-d H:i:s).
	 */
function gigneo_job_last_activity( $job_id ) {

	$job_id = absint( $job_id );

	if ( ! $job_id ) {
		return '';
	}

	global $wpdb;

	// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	$last_modified = $wpdb->get_var(
		$wpdb->prepare(
			'
			SELECT MAX( p.post_modified_gmt )
			FROM %i AS p
			INNER JOIN %i AS pm
				ON p.ID = pm.post_id
			WHERE p.post_type = %s
				AND p.post_status IN ( %s, %s, %s )
				AND pm.meta_key = %s
				AND pm.meta_value = %d
		',
			$wpdb->posts,
			$wpdb->postmeta,
			'gigneo_proposal',
			'publish',
			'shortlisted',
			'viewed',
			'_gigneo_pm_job_id',
			$job_id
		)
	);
	// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

	if ( ! empty( $last_modified ) ) {
		return $last_modified;
	}

	// Fallback: job's own modified date
	return (string) get_post_field( 'post_modified', $job_id );
}

/**
 * Display or return a saved icon.
 *
 * @param bool $should_echo Optional. Whether to echo the icon or return it. Default is false.
 * @return string The saved icon HTML if $should_echo is false, otherwise empty string.
 * @since 0.0.1
 */
function gigneo_saved_icon( $should_echo = false ) {
	// SVG output
	$svg = '<span class="gn-saved-icon" role="img">
             <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"     fill="none" stroke-width="1.5">
                 <path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"></path>
            </svg>
        </span>';

	$svg = apply_filters( 'gigneo_saved_icon', $svg );

	$allowed_html = array(
		'span' => array(
			'class'      => true,
			'role'       => true,
			'aria-label' => true,
		),
		'svg'  => array(
			'width'        => true,
			'height'       => true,
			'viewbox'      => true,
			'fill'         => true,
			'stroke'       => true,
			'stroke-width' => true,
			'xmlns'        => true,
			'class'        => true,
		),
		'path' => array(
			'd'               => true,
			'fill'            => true,
			'stroke'          => true,
			'stroke-linecap'  => true,
			'stroke-linejoin' => true,
			'fill-rule'       => true,
			'clip-rule'       => true,
		),
	);

	if ( ! $should_echo ) {
		return wp_kses( $svg, $allowed_html );
	}

	echo wp_kses( $svg, $allowed_html );
}

/**
 * Get postType name.
 *
 * @param int $post_id postType ID.
 * @since 0.0.1
 */
function gigneo_post_type_singular_name( $post_id ) {
	$post_type = get_post_type( $post_id );

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

	$post_type_obj = get_post_type_object( $post_type );

	return ! empty( $post_type_obj->labels->singular_name ) ? $post_type_obj->labels->singular_name : $post_type;
}