<?php

namespace Gigneo;

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

use DateMalformedStringException;
use DateTime;
use WP_Error;

class Notifications {


	private static $instance = null;

	/**
	 * Cron hook for daily cleanup of old notifications.
	 *
	 * @var string
	 */
	private static $cron_hook = 'gigneo_clean_old_notifications';

	private static $notification_cache_group = 'gigneo_notifications';

	private static $messages_cache_group = 'gigneo_messages';

	private static $order_messages_cache_group = 'gigneo_order_messages';

	private static $notification_table;

	private static $messages_table;

	/**
	 * Initialize notifications hooks and table references.
	 *
	 * @since 1.0.0
	 */
	private function __construct() {
		global $wpdb;

		//Set table name
		self::$notification_table = $wpdb->prefix . 'gigneo_notifications';

		self::$messages_table = $wpdb->prefix . 'gigneo_messages';

		//Get Notifications Dropdown Ajax
		add_action( 'wp_ajax_gigneo_notification_dropdown', array( $this, 'notification_dropdown' ) );

		//Get Notifications Ajax
		add_action( 'wp_ajax_gigneo_get_notifications', array( $this, 'load_notifications' ) );

		//Marked Read Notifications
		add_action( 'wp_ajax_gigneo_mark_notifications_status', array( $this, 'mark_notifications_status' ) );

		//Delete Notifications
		add_action( 'wp_ajax_gigneo_delete_notifications', array( $this, 'delete_notifications' ) );

		// Heartbeat hooks for real-time notifications.
		add_filter( 'heartbeat_received', array( $this, 'handle_heartbeat' ), 10, 3 );

		// Cron: Clean old notifications
		add_action( 'wp', array( __CLASS__, 'schedule_clean_old_notifications_cron' ) );

		// Register Cron Event: Clean old notifications
		add_action( self::$cron_hook, array( $this, 'clean_old_notifications' ) );
	}

	/**
	 * Get the notifications singleton instance.
	 *
	 * @since 1.0.0
	 *
	 * @return self
	 */
	public static function instance() {
		if ( is_null( self::$instance ) ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Get notifications for a profile with optional filters.
	 *
	 * @since 1.0.0
	 *
	 * @param int    $per_page   Number of items per page.
	 * @param int    $paged      Current page number.
	 * @param string $status     Notification status.
	 * @param string $category   Notification category.
	 * @param string $type       Notification type.
	 * @param int    $profile_id Profile ID.
	 * @param array  $meta       Meta filters.
	 *
	 * @return array|WP_Error
	 */
	public static function get_notifications( $per_page = 10, $paged = 1, $status = '', $category = '', $type = '', $profile_id = '', $meta = array() ) {
		global $wpdb;

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

		$offset = ( $paged - 1 ) * $per_page;

		$cache_key = md5(
			wp_json_encode(
				array(
					'profile_id' => $profile_id,
					'status'     => $status,
					'category'   => $category,
					'type'       => $type,
					'meta'       => $meta,
					'per_page'   => $per_page,
					'paged'      => $paged,
				)
			)
		);
		$cached    = wp_cache_get( $cache_key, self::$notification_cache_group );

		if ( false !== $cached ) {
			return $cached;
		}

		$notification_table = self::$notification_table;

		$query_args   = array( $notification_table );
		$where        = array( 'profile_id = %d' );
		$query_args[] = $profile_id;

		if ( in_array( $status, array( 'read', 'unread' ), true ) ) {
			$where[]      = 'status = %s';
			$query_args[] = $status;
		}

		if ( ! empty( $category ) ) {
			$where[]      = 'category = %s';
			$query_args[] = $category;
		}

		if ( ! empty( $type ) ) {
			$where[]      = 'type = %s';
			$query_args[] = $type;
		}

		if ( ! empty( $meta ) ) {
			foreach ( $meta as $key => $value ) {
				$where[]      = 'meta LIKE %s';
				$query_args[] = '%' . $wpdb->esc_like( $key ) . ':' . $wpdb->esc_like( $value ) . '%';
			}
		}

		$where_clause = ! empty( $where ) ? ' WHERE ' . implode( ' AND ', $where ) : '';

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$total       = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i{$where_clause}", $query_args ) );
		$total_pages = $per_page > 0 ? ceil( $total / $per_page ) : 1;
		$results     = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM %i{$where_clause} ORDER BY created_at_gmt DESC LIMIT %d OFFSET %d",
				array_merge( $query_args, array( $per_page, $offset ) )
			),
			ARRAY_A
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, PluginCheck.Security.DirectDB.UnescapedDBParameter

		if ( false === $results ) {
			return new WP_Error( 'db_error', esc_html__( 'Database query failed.', 'gigneo' ) );
		}

		$notifications = array_map(
			function ( $row ) {
				$row['meta'] = maybe_unserialize( $row['meta'] ?? '' );
				return $row;
			},
			$results
		);

		$response = array(
			'notifications' => $notifications,
			'total'         => $total,
			'total_pages'   => $total_pages,
			'current_page'  => $paged,
		);

		wp_cache_set( $cache_key, $response, self::$notification_cache_group, HOUR_IN_SECONDS );

		return apply_filters( 'gigneo_get_notifications', $response, $per_page, $paged, $status, $profile_id );
	}

	/**
	 * Insert or reuse a notification entry and trigger realtime update.
	 *
	 * @since 1.0.0
	 *
	 * @param string $title      Notification title.
	 * @param string $content    Notification content.
	 * @param string $category   Notification category.
	 * @param string $type       Notification type.
	 * @param int    $profile_id Profile ID.
	 * @param int    $post_id    Related post ID.
	 * @param array  $meta       Notification metadata.
	 *
	 * @return int|WP_Error
	 */
	public static function trigger_notification( $title, $content, $category, $type, $profile_id, $post_id = '', $meta = array() ) {
		global $wpdb;

		if ( empty( $profile_id ) ) {
			return new WP_Error( 'invalid_profile', __( 'Profile ID is required.', 'gigneo' ) );
		}

		$table = self::$notification_table;
		$now   = current_time( 'mysql', true );

		$meta_data = maybe_serialize( $meta );

		if ( strlen( $meta_data ) > 255 ) {
			return new WP_Error( 'meta_too_long', __( 'Meta data exceeds 255 characters.', 'gigneo' ) );
		}

		$same_last_notification_id = 0;
		$data                      = null;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$last_notification = $wpdb->get_row( $wpdb->prepare( 'SELECT id, post_id, title, content, category, type, status, meta FROM %i WHERE profile_id = %d ORDER BY id DESC LIMIT 1', $table, $profile_id ) );
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		if ( $last_notification && $title === $last_notification->title && $content === $last_notification->content && $category === $last_notification->category && $type === $last_notification->type && 'unread' === $last_notification->status && $last_notification->post_id === $post_id ) {
			$same_last_notification_id = $last_notification->id;
		}

		if ( empty( $same_last_notification_id ) ) {
			$data = array(
				'profile_id'     => absint( $profile_id ),
				'post_id'        => absint( $post_id ),
				'title'          => sanitize_text_field( $title ),
				'content'        => wp_kses( $content, gigneo_allowed_html_tags() ),
				'category'       => sanitize_key( $category ),
				'type'           => sanitize_key( $type ),
				'status'         => 'unread',
				'meta'           => $meta_data,
				'created_at'     => $now,
				'created_at_gmt' => gmdate( 'Y-m-d H:i:s' ),
				'updated_at'     => $now,
				'updated_at_gmt' => gmdate( 'Y-m-d H:i:s' ),
			);

			$format = array(
				'%d', // profile_id
				'%d', // post_id
				'%s', // title
				'%s', // content
				'%s', // category
				'%s', // type
				'%s', // status
				'%s', // meta
				'%s', // created_at
				'%s', // created_at_gmt
				'%s', // updated_at
				'%s', // updated_at_gmt
			);

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$result = $wpdb->insert( $table, $data, $format );

			if ( false === $result ) {
				return new WP_Error( 'db_error', esc_html__( 'Failed to insert notification.', 'gigneo' ) );
			}

			$notification_id = $wpdb->insert_id;
		} else {
			$notification_id = $same_last_notification_id;
		}

		// Clear cache
		wp_cache_flush_group( self::$notification_cache_group );

		do_action( 'gigneo_notification_triggered', $notification_id, $data );

		return $notification_id;
	}

	/**
	 * Get unread notification count for a profile.
	 *
	 * @since 1.0.0
	 *
	 * @param int $profile_id Profile ID.
	 *
	 * @return int
	 */
	public static function get_unread_notification_count( $profile_id = '' ) {
		global $wpdb;

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

		$cache_key = md5( "gigneo_unread_notification_count_$profile_id" );
		$cached    = wp_cache_get( $cache_key, self::$notification_cache_group );

		if ( false !== $cached ) {
			return $cached;
		}

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$unread_count = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE profile_id = %d AND status = %s', self::$notification_table, $profile_id, 'unread' ) );
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		if ( false === $unread_count ) {
			return 0;
		}

		wp_cache_set( $cache_key, (int) $unread_count, self::$notification_cache_group, HOUR_IN_SECONDS );

		return (int) $unread_count;
	}

	/**
	 * Get unread notification count for a profile.
	 *
	 * @since 1.0.0
	 *
	 * @param int $profile_id Profile ID.
	 *
	 * @return int
	 */
	public static function get_unread_order_messages_count( $profile_id = '', $order_id = '' ) {
		global $wpdb;

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

		if ( empty( $order_id ) ) {
			return 0;
		}

		$cache_key = md5( "gigneo_order_messages_unread_count_{$profile_id}_{$order_id}" );
		$cached    = wp_cache_get( $cache_key, self::$order_messages_cache_group );

		if ( false !== $cached ) {
			return (int) $cached;
		}

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$order_messages_count = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE receiver_id = %d AND status = %s AND type = %s AND post_id = %d', self::$messages_table, $profile_id, 'unread', 'order', $order_id ) );
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		if ( false === $order_messages_count ) {
			$order_messages_count = 0;
		}

		wp_cache_set( $cache_key, (int) $order_messages_count, self::$order_messages_cache_group, HOUR_IN_SECONDS );

		return (int) $order_messages_count;
	}

	/**
	 * Mark one or more notifications as read or unread.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function mark_notifications_status() {
		global $wpdb;

		$security = isset( $_POST['security'] ) ? sanitize_text_field( wp_unslash( $_POST['security'] ) ) : '';
		if ( empty( $security ) || ! wp_verify_nonce( $security, 'gigneo_security' ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Security verification failed. Please reload and try again.', 'gigneo' ) ), 403 );
		}

		$platform_access = gigneo_user_have_access();
		$profile_id      = gigneo_active_profile_id();
		$current_user    = wp_get_current_user();

		if ( empty( $current_user->ID ) || ! $platform_access || empty( $profile_id ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'You do not have permission to do that.', 'gigneo' ) ), 403 );
		}

		$ids    = isset( $_POST['ids'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['ids'] ) ) : array();
		$status = isset( $_POST['status'] ) ? sanitize_text_field( wp_unslash( $_POST['status'] ) ) : 'read';

		if ( ! in_array( $status, array( 'read', 'unread' ), true ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Invalid status value.', 'gigneo' ) ), 400 );
		}

		if ( empty( $ids ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'No valid notification IDs provided.', 'gigneo' ) ), 400 );
		}

		$table = self::$notification_table;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
		$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
		$found_ids    = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM %i WHERE id IN ($placeholders) AND profile_id = %d",
				array_merge( array( $table ), array_map( 'absint', $ids ), array( $profile_id ) )
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

		if ( empty( $found_ids ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'No notifications found for this profile.', 'gigneo' ) ), 400 );
		}

		if ( count( $found_ids ) !== count( $ids ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'You do not have permission to modify one or more notifications.', 'gigneo' ) ), 403 );
		}

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
		$placeholders_updated = implode( ',', array_fill( 0, count( $found_ids ), '%d' ) );
		$updated              = $wpdb->query(
			$wpdb->prepare(
				"UPDATE %i SET status = %s WHERE id IN ($placeholders_updated) AND profile_id = %d",
				array_merge( array( $table, $status ), array_map( 'absint', $found_ids ), array( $profile_id ) )
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

		wp_cache_flush_group( self::$notification_cache_group );

		if ( false === $updated ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Failed to update notifications.', 'gigneo' ) ), 400 );
		}

		$message = ( 'read' === $status )
			? esc_html__( 'Notification(s) marked as read successfully.', 'gigneo' )
			: esc_html__( 'Notification(s) marked as unread successfully.', 'gigneo' );

		do_action( 'gigneo_notifications_status_updated', $profile_id, $status, $found_ids );

		wp_send_json_success(
			array(
				'message'       => $message,
				'status'        => $status,
				'updated_count' => (int) $updated,
				'ids'           => $found_ids,
			)
		);
	}

	/**
	 * Delete one or more notifications.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function delete_notifications() {
		global $wpdb;

		$security = isset( $_POST['security'] ) ? sanitize_text_field( wp_unslash( $_POST['security'] ) ) : '';
		if ( empty( $security ) || ! wp_verify_nonce( $security, 'gigneo_security' ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Security verification failed. Please reload and try again.', 'gigneo' ) ), 403 );
		}

		$platform_access = gigneo_user_have_access();
		$profile_id      = gigneo_active_profile_id();
		$current_user    = wp_get_current_user();

		if ( empty( $current_user->ID ) || ! $platform_access || empty( $profile_id ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'You do not have permission to do that.', 'gigneo' ) ), 403 );
		}

		$ids = isset( $_POST['ids'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['ids'] ) ) : array();

		if ( empty( $ids ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'No valid notification IDs provided.', 'gigneo' ) ), 400 );
		}

		$table = self::$notification_table;

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
		$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
		$found_ids    = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT id FROM %i WHERE id IN ($placeholders) AND profile_id = %d",
				array_merge( array( $table ), array_map( 'absint', $ids ), array( $profile_id ) )
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

		if ( empty( $found_ids ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'No notifications found for this profile.', 'gigneo' ) ), 400 );
		}

		if ( count( $found_ids ) !== count( $ids ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'You do not have permission to delete one or more notifications.', 'gigneo' ) ), 403 );
		}

		// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
		$placeholders_deleted = implode( ',', array_fill( 0, count( $found_ids ), '%d' ) );
		$deleted              = $wpdb->query(
			$wpdb->prepare(
				"DELETE FROM %i WHERE id IN ($placeholders_deleted) AND profile_id = %d",
				array_merge( array( $table ), array_map( 'absint', $found_ids ), array( $profile_id ) )
			)
		);
		// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

		wp_cache_flush_group( self::$notification_cache_group );

		if ( false === $deleted ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Failed to delete notifications.', 'gigneo' ) ), 400 );
		}

		do_action( 'gigneo_notifications_deleted', $profile_id, $found_ids );

		wp_send_json_success(
			array(
				'message'       => esc_html__( 'Notification(s) deleted successfully.', 'gigneo' ),
				'deleted_count' => $deleted,
				'ids'           => $found_ids,
			)
		);
	}

	/**
	 * Load notifications markup for AJAX response.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function load_notifications() {
		// Check if nonce is valid
		$security = isset( $_GET['security'] ) ? sanitize_text_field( wp_unslash( $_GET['security'] ) ) : '';
		if ( empty( $security ) || ! wp_verify_nonce( $security, 'gigneo_security' ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Invalid security token.', 'gigneo' ) ), 403 );
		}

		$platform_access = gigneo_user_have_access();
		$profile_id      = gigneo_active_profile_id();
		$current_user    = wp_get_current_user();

		if ( empty( $current_user->ID ) || ! $platform_access || empty( $profile_id ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'You do not have permission to do that.', 'gigneo' ) ), 403 );
		}

		$paged    = isset( $_GET['paged'] ) ? absint( $_GET['paged'] ) : 1;
		$per_page = isset( $_GET['per_page'] ) ? absint( $_GET['per_page'] ) : 10;
		$ref      = isset( $_GET['ref'] ) ? sanitize_text_field( wp_unslash( $_GET['ref'] ) ) : '';

		$args = array(
			'per_page' => $per_page,
			'paged'    => $paged,
			'ref'      => $ref,
		);

		ob_start();
		gigneo_load_template( 'dashboard/notification', false, $args );
		$notification_markup = ob_get_clean();

		$response                           = array();
		$response['markup']['notification'] = $notification_markup;

		wp_send_json_success( $response );
	}

	/**
	 * Load notification dropdown markup for AJAX response.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function notification_dropdown() {
		// Check if nonce is valid
		$security = isset( $_GET['security'] ) ? sanitize_text_field( wp_unslash( $_GET['security'] ) ) : '';
		if ( empty( $security ) || ! wp_verify_nonce( $security, 'gigneo_security' ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'Invalid security token.', 'gigneo' ) ), 403 );
		}

		$platform_access = gigneo_user_have_access();
		$profile_id      = gigneo_active_profile_id();
		$current_user    = wp_get_current_user();

		if ( empty( $current_user->ID ) || ! $platform_access || empty( $profile_id ) ) {
			wp_send_json_error( array( 'message' => esc_html__( 'You do not have permission to do that.', 'gigneo' ) ), 403 );
		}

		ob_start();
		gigneo_load_template( 'dropdown/notification', false );
		$notification_markup = ob_get_clean();

		$response                           = array();
		$response['markup']['notification'] = $notification_markup;

		wp_send_json_success( $response );
	}

	/**
	 * Handle heartbeat payload for notifications and messages.
	 *
	 * @since 1.0.0
	 *
	 * @param array  $response  Heartbeat response.
	 * @param array  $data      Heartbeat request data.
	 *
	 * @return array
	 */
	public function handle_heartbeat( $response, $data ) {

		global $wpdb;
		$profile_id = gigneo_active_profile_id();

		// Get Unread Notifications Count
		if ( isset( $data['gigneo_notifications'] ) ) {

			$count_cache_key = md5( "gigneo_messages_unread_count_$profile_id" );
			$unread_count    = wp_cache_get( $count_cache_key, self::$notification_cache_group );

			if ( false === $unread_count ) {
				// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$unread_count = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE profile_id = %d AND status = %s', self::$notification_table, $profile_id, 'unread' ) );
				// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

				if ( false !== $unread_count ) {
					wp_cache_set(
						$count_cache_key,
						(int) $unread_count,
						self::$notification_cache_group,
						HOUR_IN_SECONDS
					);
				}
			}

			if ( false !== $unread_count ) {
				$response['gigneo_unread_notification_count'] = (int) $unread_count;
			}
		}

		// Get Onscreen Notifications (Filtered by Type)
		if ( ! empty( $data['gigneo_onscreen_notifications'] ) ) {

			$last_check_raw = sanitize_text_field( $data['gigneo_onscreen_notifications']['last_check'] );
			$last_check     = gmdate( 'Y-m-d H:i:s', strtotime( $last_check_raw ) );

			$onscreen_notification_option = gigneo_option( '_gigneo_ts_notification_onscreen', true );

			// User notification preferences
			$notification_map = array(
				'order'    => gigneo_post_meta( $profile_id, '_gigneo_pm_notification_order' ),
				'chat'     => gigneo_post_meta( $profile_id, '_gigneo_pm_notification_chat' ),
				'ticket'   => gigneo_post_meta( $profile_id, '_gigneo_pm_notification_support' ),
				'service'  => gigneo_post_meta( $profile_id, '_gigneo_pm_notification_service' ),
				'job'      => gigneo_post_meta( $profile_id, '_gigneo_pm_notification_job' ),
				'withdraw' => gigneo_post_meta( $profile_id, '_gigneo_pm_notification_withdraw' ),
				'wallet'   => gigneo_post_meta( $profile_id, '_gigneo_pm_notification_wallet' ),
				'account'  => gigneo_post_meta( $profile_id, '_gigneo_pm_notification_account' ),
			);

			if ( $onscreen_notification_option ) {

				$onscreen_cache_key     = md5( "gigneo_onscreen_notifications_{$profile_id}_$last_check" );
				$onscreen_notifications = wp_cache_get( $onscreen_cache_key, self::$notification_cache_group );

				if ( false === $onscreen_notifications ) {

					// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
					$onscreen_notifications = $wpdb->get_results( $wpdb->prepare( 'SELECT title, content, category, type, post_id FROM %i WHERE profile_id = %d AND created_at_gmt > %s AND status = %s ORDER BY created_at_gmt DESC LIMIT 10', self::$notification_table, $profile_id, $last_check, 'unread' ), ARRAY_A );
					// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

					if ( ! empty( $onscreen_notifications ) ) {

						foreach ( $onscreen_notifications as $key => &$notification ) {

							$notification['title']   = html_entity_decode( $notification['title'] ?? '' );
							$notification['content'] = html_entity_decode( $notification['content'] ?? '' );
							$notification['type']    = html_entity_decode( $notification['type'] ?? '' );
							$notification['post_id'] = absint( $notification['post_id'] ?? 0 );

							$type = $notification['category'];

							// Remove notification if user disabled this type
							if ( isset( $notification_map[ $type ] ) && 'no' === $notification_map[ $type ] ) {
								unset( $onscreen_notifications[ $key ] );
							}
						}
						unset( $notification );

						// Reindex array
						$onscreen_notifications = array_values( $onscreen_notifications );

						// Cache filtered result
						wp_cache_set(
							$onscreen_cache_key,
							$onscreen_notifications,
							self::$notification_cache_group,
							HOUR_IN_SECONDS
						);
					}
				}

				if ( ! empty( $onscreen_notifications ) ) {
					$response['gigneo_onscreen_notifications'] = $onscreen_notifications;
				}
			}
		}

		// Get Unread Messages Count
		if ( isset( $data['gigneo_messages'] ) ) {

			$count_cache_key = md5( "gigneo_messages_unread_count_$profile_id" );
			$unread_count    = wp_cache_get( $count_cache_key, self::$messages_cache_group );

			if ( false === $unread_count ) {

				// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$unread_count = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE receiver_id = %d AND status = %s AND type = %s', self::$messages_table, $profile_id, 'unread', 'chat' ) );
				// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

				if ( false !== $unread_count ) {
					wp_cache_set(
						$count_cache_key,
						(int) $unread_count,
						self::$messages_cache_group,
						HOUR_IN_SECONDS
					);
				}
			}

			if ( false !== $unread_count ) {
				$response['gigneo_unread_messages_count'] = (int) $unread_count;
			}
		}

		// Get Unread Order Messages Count
		if ( isset( $data['gigneo_order_messages'] ) ) {

			$count_cache_key      = md5( "gigneo_order_messages_unread_count_$profile_id" );
			$order_messages_count = wp_cache_get( $count_cache_key, self::$order_messages_cache_group );

			if ( false === $order_messages_count ) {

				// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
				$order_messages_count = $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE receiver_id = %d AND status = %s AND type = %s', self::$messages_table, $profile_id, 'unread', 'order' ) );
				// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

				if ( false !== $order_messages_count ) {
					wp_cache_set(
						$count_cache_key,
						(int) $order_messages_count,
						self::$order_messages_cache_group,
						HOUR_IN_SECONDS
					);
				}
			}

			if ( false !== $order_messages_count ) {
				$response['gigneo_order_messages_count'] = (int) $order_messages_count;
			}
		}

		return $response;
	}

	/**
	 * Get icon metadata for a notification type.
	 *
	 * @since 1.0.0
	 *
	 * @param string $notification_type Notification type.
	 *
	 * @return array
	 */
	public static function icon( $notification_type ) {
		$icons = array(
			'register_account'       => array(
				'icon'   => 'hi hi-user',
				'status' => 'gn-info',
			),
			'email_verified'         => array(
				'icon'   => 'hi hi-check-badge',
				'status' => 'gn-success',
			),
			'withdrawal-in-progress' => array(
				'icon'   => 'hi hi-credit-card',
				'status' => 'gn-warning',
			),
			'withdrawal-approved'    => array(
				'icon'   => 'hi hi-credit-card',
				'status' => 'gn-success',
			),
			'withdrawal-rejected'    => array(
				'icon'   => 'hi hi-credit-card',
				'status' => 'gn-success',
			),
			'withdrawal-on-hold'     => array(
				'icon'   => 'hi hi-credit-card',
				'status' => 'gn-success',
			),
			'credit'                 => array(
				'icon'   => 'hi hi-credit-card',
				'status' => 'gn-info',
			),
			'status'                 => array(
				'icon'   => 'hi hi hi-shield-exclamation',
				'status' => 'gn-info',
			),
			'funds_added'            => array(
				'icon'   => 'hi hi-wallet',
				'status' => 'gn-success',
			),
			'credit_purchased'       => array(
				'icon'   => 'hi hi-credit-card',
				'status' => 'gn-success',
			),
			'proposal_submitted'     => array(
				'icon'   => 'hi hi-document-text',
				'status' => 'gn-info',
			),
			'identity_approved'      => array(
				'icon'   => 'hi hi-identification',
				'status' => 'gn-success',
			),
			'identity_rejected'      => array(
				'icon'   => 'hi hi-identification',
				'status' => 'gn-danger',
			),
			'report_submitted'       => array(
				'icon'   => 'hi hi-flag',
				'status' => 'gn-info',
			),
			'report_closed'          => array(
				'icon'   => 'hi hi-flag',
				'status' => 'gn-success',
			),
			'report_dismissed'       => array(
				'icon'   => 'hi hi-flag',
				'status' => 'gn-danger',
			),
			'unblocked'              => array(
				'icon'   => 'hi hi-lock-open',
				'status' => 'gn-success',
			),
			'blocked'                => array(
				'icon'   => 'hi hi-lock-closed',
				'status' => 'gn-danger',
			),
		);

		$icons = apply_filters( 'gigneo_notification_icons', $icons, $notification_type );

		$icon = $icons[ $notification_type ] ?? array(
			'icon'   => 'hi hi-exclamation-circle',
			'status' => 'general',
		);

		return apply_filters( 'gigneo_notification_icon', $icon, $notification_type );
	}

	/**
	 * Get avatar thumbnail id for a notification context.
	 *
	 * @since 1.0.0
	 *
	 * @param string $notification_category   Notification category.
	 * @param string $notification_type       Notification type.
	 * @param int    $notification_profile_id Related profile ID.
	 * @param int    $notification_post_id    Related post ID.
	 *
	 * @return int|string|false
	 */
	public static function avatar( $notification_category, $notification_type, $notification_profile_id = '', $notification_post_id = '' ) {
		$profile_id   = gigneo_active_profile_id();
		$thumbnail_id = false;
		$profile_type = gigneo_profile_type( $profile_id );

		if ( 'order' === $notification_category ) {
			$participant_profile_type     = 'freelancer' === $profile_type ? 'employer' : 'freelancer';
			$participant_profile_meta_key = 'employer' === $participant_profile_type ? '_gigneo_pm_profile_id' : '_gigneo_pm_freelancer_profile_id';
			$participant_profile_id       = gigneo_post_meta( $notification_post_id, $participant_profile_meta_key );

			if ( ! empty( $participant_profile_id ) && absint( $profile_id ) !== absint( $participant_profile_id ) ) {
				$thumbnail_id = get_post_thumbnail_id( $participant_profile_id );
				$thumbnail_id = ! empty( $thumbnail_id ) ? $thumbnail_id : '';
			}
		} elseif ( 'ticket' === $notification_category || 'bulk_assignment' === $notification_category ) {
			$profile_avatar_id = gigneo_moderator_avatar_id();
			$thumbnail_id      = $profile_avatar_id ?? '';
		} elseif ( 'message' === $notification_category ) {
			$participant_profile_id = $notification_post_id;
			if ( ! empty( $participant_profile_id ) && absint( $profile_id ) !== absint( $participant_profile_id ) ) {
				$thumbnail_id = get_post_thumbnail_id( $participant_profile_id );
				$thumbnail_id = ! empty( $thumbnail_id ) ? $thumbnail_id : '';
			}
		} elseif ( 'proposal' === $notification_category ) {
			$participant_profile_id = gigneo_post_meta( $notification_post_id, '_gigneo_pm_freelancer_profile_id' );
			if ( ! empty( $participant_profile_id ) && absint( $profile_id ) !== absint( $participant_profile_id ) ) {
				$thumbnail_id = get_post_thumbnail_id( $participant_profile_id );
				$thumbnail_id = ! empty( $thumbnail_id ) ? $thumbnail_id : '';
			}
		}
		return apply_filters( 'gigneo_notification_avatar', $thumbnail_id, $notification_category, $notification_type, $notification_profile_id, $notification_post_id );
	}

	/**
	 * Get target link data for a notification action.
	 *
	 * @since 1.0.0
	 *
	 * @param string $notification_category   Notification category.
	 * @param string $notification_type       Notification type.
	 * @param int    $notification_profile_id Related profile ID.
	 * @param int    $notification_post_id    Related post ID.
	 *
	 * @return array
	 */
	public static function link( $notification_category, $notification_type, $notification_profile_id = '', $notification_post_id = '' ) {

		$button = array(
			'link'   => '',
			'target' => '_self',
		);

		if ( 'order' === $notification_category && ! empty( $notification_post_id ) ) {
			$order_type   = gigneo_post_meta( $notification_post_id, '_gigneo_pm_order_type' );
			$service_slug = Post_Types::post_type_slug( 'gigneo_service' ) ?? 'service';
			$job_slug     = Post_Types::post_type_slug( 'gigneo_job' ) ?? 'job';
			$order_slug   = 'service' === $order_type ? $service_slug : $job_slug;

			$button['link'] = gigneo_dashboard_url(
				'order',
				"$order_slug-detail",
				array(
					'tab'  => 'activity',
					'p_id' => $notification_post_id,
				)
			);

			if ( 'message' === $notification_type ) {
				$button['link'] = gigneo_dashboard_url(
					'order',
					"$order_slug-detail",
					array(
						'tab'  => 'messages',
						'p_id' => $notification_post_id,
					)
				);
			} elseif ( 'requirement_pending' === $notification_type ) {
				$button['link'] = gigneo_dashboard_url(
					'order',
					"$order_slug-detail",
					array(
						'tab'  => 'requirements',
						'p_id' => $notification_post_id,
					)
				);
			}
		} elseif ( 'chat' === $notification_category && ! empty( $notification_profile_id ) ) {
			$button['link'] = gigneo_dashboard_url( 'message', 'list', array( 'p_id' => $notification_profile_id ) );
		} elseif ( 'ticket' === $notification_category && ! empty( $notification_post_id ) ) {
			$button['link'] = gigneo_dashboard_url( 'support', 'detail', array( 'p_id' => $notification_post_id ) );
		} elseif ( 'account' === $notification_category && ! empty( $notification_post_id ) ) {
			$button['link'] = gigneo_dashboard_url( 'setting', 'profile', array( 'p_id' => $notification_post_id ) );
		} elseif ( 'wallet' === $notification_category && ! empty( $notification_post_id ) ) {
			$button['link'] = gigneo_dashboard_url( 'account', 'wallet' );
		} elseif ( 'proposal' === $notification_category && ! empty( $notification_post_id ) ) {
			$button['link'] = gigneo_dashboard_url( 'proposal', 'detail', array( 'p_id' => $notification_post_id ) );
		}

		return apply_filters( 'gigneo_notification_button', $button, $notification_category, $notification_type, $notification_profile_id, $notification_post_id );
	}

	/**
	 * Format notification date for display.
	 *
	 * @since 1.0.0
	 *
	 * @param string $notification_created_at Notification created date in GMT.
	 * @param int    $notification_profile_id Notification profile ID.
	 *
	 * @return string
	 */
	public static function date( $notification_created_at, $notification_profile_id = '' ) {
		$profile_id = gigneo_active_profile_id();

		// Get today and yesterday dates in profile timezone
		$today_in_profile_tz     = gigneo_datetime( current_time( 'mysql', 1 ), 'Y-m-d', $profile_id );
		$yesterday_in_profile_tz = gmdate( 'Y-m-d', strtotime( $today_in_profile_tz . ' -1 day' ) );
		$notification_date_only  = gigneo_datetime( $notification_created_at, 'Y-m-d', $profile_id );

		// Time string in profile timezone
		$time_str = gigneo_datetime( $notification_created_at, get_option( 'time_format' ), $profile_id );

		if ( $notification_date_only === $today_in_profile_tz ) {
			// Today
			$notification_date = sprintf(
				// translators: %s is the time
				esc_html__( 'Today at %s', 'gigneo' ),
				$time_str
			);

		} elseif ( $notification_date_only === $yesterday_in_profile_tz ) {
			// Yesterday
			$notification_date = sprintf(
				// translators: %s is the time
				esc_html__( 'Yesterday at %s', 'gigneo' ),
				$time_str
			);

		} else {
			// Older
			$notification_date = gigneo_datetime( $notification_created_at, get_option( 'date_format' ) );
		}

		return apply_filters( 'gigneo_notification_date', $notification_date, $notification_created_at, $notification_profile_id );
	}

	/**
	 * Schedule the daily cleanup event for old notifications.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public static function schedule_clean_old_notifications_cron() {
		if ( wp_next_scheduled( self::$cron_hook ) ) {
			return;
		}

		$timezone = wp_timezone();
		$datetime = new DateTime( 'today 00:00', $timezone );
		$start    = $datetime->getTimestamp();

		if ( $start <= time() ) {
			$datetime->modify( '+1 day' );
			$start = $datetime->getTimestamp();
		}

		wp_schedule_event( $start, 'daily', self::$cron_hook );
	}

	/**
	 * Clear the daily cleanup cron for old notifications.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public static function clear_clean_old_notifications_cron() {
		wp_clear_scheduled_hook( self::$cron_hook );
	}

	/**
	 * Delete old read notifications in batches.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function clean_old_notifications() {
		global $wpdb;
		$days       = absint( gigneo_option( '_gigneo_ts_notification_auto_delete', 15 ) );
		$batch_size = 1000;

		do {
			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
			$ids = $wpdb->get_col( $wpdb->prepare( 'SELECT id FROM %i WHERE status = %s AND created_at_gmt < %s LIMIT %d', self::$notification_table, 'read', gmdate( 'Y-m-d H:i:s', strtotime( "-$days days" ) ), $batch_size ) );
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

			if ( empty( $ids ) ) {
				break;
			}

			// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
			$placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) );
			$wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE id IN ($placeholders)", array_merge( array( self::$notification_table ), array_map( 'absint', $ids ) ) ) );
			// phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber

			$ids_count = count( $ids );
		} while ( $ids_count === $batch_size );

		wp_cache_flush();
	}
}
