Jake Vanderwerf
4 hours ago 56a9a1ccf764ff7a6af8f8a2292cb07443cb4aa7
inc/rest/routes/NotificationsRoutes.php
@@ -1,1754 +1,1496 @@
<?php
namespace JVBase\rest\routes;
use JVBase\JVB;
use JVBase\rest\RestRouteManager;
use JVBase\managers\Cache;
use JVBase\managers\CustomTable;
use JVBase\registrar\Registrar;
use JVBase\rest\Rest;
use JVBase\rest\Route;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use Exception;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
   exit; // Exit if accessed directly
}
/**
 * Step 1: Build status/order/filter params
 * Step 2: Get all regular notifications
 * Step 3: Get all Content notifications
 * Step 4: Get all Approval notifications
 * Step 5: Merge in order of created date
 * Step 6: Return result
 * Notification Routes Handler
 *
 * Manages user notifications including regular notifications, content notifications,
 * and approval notifications. Provides endpoints for reading, marking, and dismissing.
 */
class NotificationsRoutes extends RestRouteManager
class NotificationsRoutes extends Rest
{
    protected int $user_id;
    protected array $notification_types = [];
    protected object $manager;
    protected array $typeMap = [
        'favourite' => [
            'new_favourite',
            'list_shared',
        ],
        'artist' => [
            'new_artist',
            'new_tattoo',
            'new_piercing',
            'new_event',
            'new_update',
   protected int $user_id;
   protected array $notification_types = [];
   protected object $manager;
        ],
        'partner'   => [
            'new_partner',
            'new_offer',
        ],
        'shop' => [
            'new_shop',
            'shop_update',
            'shop_accepted',
            'artist_request',
        ],
        'event' => [
            'new_event',
            'event_reminder',
        ],
        'news' => [
            'new_update',
        ],
        'system' => [
            'system_message',
            'artist_approved',
            'artist_invitation',
            'artist_request',
            'shop_accepted',
            'shop_rejected',
            'new_term',
            'term_approved',
            'term_rejected',
        ],
    ];
    protected array $notificationTableMap = [
        // Regular notifications
        'notifications' => [
            'new_favourite',
            'artist_approved',
            'artist_rejected',
            'artist_invitation',
            'shop_invitation',
            'artist_request',
            'shop_accepted',
            'shop_rejected',
            'new_term',
            'term_approved',
            'term_rejected',
            'list_shared',
            'system_message'
        ],
   protected array $typeMap = [
      'favourite' => [
         'new_favourite',
         'list_shared',
      ],
      'artist' => [
         'new_artist',
         'new_tattoo',
         'new_piercing',
         'new_event',
         'new_update',
      ],
      'partner' => [
         'new_partner',
         'new_offer',
      ],
      'shop' => [
         'new_shop',
         'shop_update',
         'shop_accepted',
         'artist_request',
      ],
      'event' => [
         'new_event',
         'event_reminder',
      ],
      'news' => [
         'new_update',
      ],
      'system' => [
         'system_message',
         'artist_approved',
         'artist_invitation',
         'artist_request',
         'shop_accepted',
         'shop_rejected',
         'new_term',
         'term_approved',
         'term_rejected',
      ],
   ];
        // Content notifications (from artists the user follows)
        'content_notifications' => [
            'new_artist',
            'new_tattoo',
            'new_piercing',
            'new_event',
            'new_update',
            'new_partner',
            'new_offer',
            'new_shop',
            'shop_update',
            'event_reminder'
        ]
    ];
   protected array $notificationTableMap = [
      'notifications' => [
         'new_favourite',
         'artist_approved',
         'artist_rejected',
         'artist_invitation',
         'shop_invitation',
         'artist_request',
         'shop_accepted',
         'shop_rejected',
         'new_term',
         'term_approved',
         'term_rejected',
         'list_shared',
         'system_message'
      ],
      'content_notifications' => [
         'new_artist',
         'new_tattoo',
         'new_piercing',
         'new_event',
         'new_update',
         'new_partner',
         'new_offer',
         'new_shop',
         'shop_update',
         'event_reminder'
      ]
   ];
    public function __construct()
    {
        $this->cache_name = 'notifications';
        parent::__construct();
   protected CustomTable $notifications;
   protected CustomTable $metrics;
        $allTypes = [];
        foreach ($this->typeMap as $key => $values) {
            $allTypes = array_unique(array_merge($allTypes, $values));
        }
   public function __construct()
   {
      $this->cacheName = 'notifications';
      $this->cacheTtl = HOUR_IN_SECONDS;
      parent::__construct();
        $this->typeMap['all'] = $allTypes;
      // Connect notifications cache to user cache
      // When user data changes, notification cache should invalidate
      $this->cache->connect('user');
        $this->user_id = get_current_user_id();
        $this->action = 'notifications-';
      $this->notifications = CustomTable::for('notifications');
      $this->metrics = CustomTable::for('notification_metrics');
        add_action('init', [$this, 'init']);
      // Build complete type map
      $allTypes = [];
      foreach ($this->typeMap as $key => $values) {
         $allTypes = array_unique(array_merge($allTypes, $values));
      }
      $this->typeMap['all'] = $allTypes;
        add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
    }
    /**
     * Format a notification for display
     *
     * @param object $notification Notification object
     *
     * @return array Formatted notification
     */
    protected function formatNotification(object $notification):array
    {
        $config = $this->notification_types[$notification->type] ?? [];
        $context = json_decode($notification->context ?? '{}', true);
      $this->user_id = get_current_user_id();
        // Get action user's name if available
        $acting_user_name = null;
        if ($notification->action_user_id) {
            $acting_user_name = jvbShareName($notification->action_user_id);
        }
      add_action('init', [$this, 'init']);
      add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
   }
        return [
            'id' => $notification->id,
            'type' => $notification->type,
            'message' => $notification->message,
            'created_at' => $notification->created_at,
            'status' => $notification->status,
            'requires_action' => (bool)$notification->requires_action,
            'action_taken' => (bool)$notification->action_taken,
            'icon' => $config['icon'] ?? 'info',
            'priority' => $notification->priority,
            'target' => [
                'id' => $notification->target_id,
                'type' => $notification->target_type
            ],
            'context' => $context,
            'acting_user' => $notification->action_user_id ? [
                'id' => $notification->action_user_id,
                'name' => $acting_user_name
            ] : null
        ];
    }
   /**
    * Set up required parameters
    */
   public function init(): void
   {
      $this->manager = JVB()->notification();
      $this->notification_types = $this->manager->getNotificationTypes(true);
   }
    /**
     * Set up required paramaters
     * @return void
     */
    public function init()
    {
        $this->manager = JVB()->notification();
        $this->notification_types = $this->manager->getNotificationTypes();
    }
   /**
    * Register notification routes
    */
   public function registerRoutes(): void
   {
      // Get user notifications
      Route::for('notifications')
         ->get([$this, 'getNotifications'])
         ->args([
            'user' => 'integer|required',
            'type' => 'string',
            'status' => 'string|enum:unread,read,actioned,dismissed',
            'limit' => 'integer|default:20|min:1|max:100',
            'offset' => 'integer|default:0',
         ])
         ->auth('user')
         ->rateLimit(30)
         ->register();
    /**
     * Registers notification routes
     * @return void
     */
    public function registerRoutes():void
    {
        register_rest_route($this->namespace, '/notifications', [
            [
                'methods' => 'GET',
                'callback' => [$this, 'getNotifications'],
                'permission_callback' => [$this, 'checkPermission']
            ],
            [
                'methods' => 'POST',
                'callback' => [$this, 'updateNotifications'],
                'permission_callback' => [$this, 'checkPermission']
            ]
        ]);
    }
      // Mark as read
      Route::for('notifications/read')
         ->post([$this, 'markRead'])
         ->args([
            'user' => 'integer|required',
            'notification_id' => 'integer|required',
         ])
         ->auth('user')
         ->rateLimit(30)
         ->register();
      // Mark all as read
      Route::for('notifications/read-all')
         ->post([$this, 'markAllRead'])
         ->args([
            'user' => 'integer|required',
            'type' => 'string',
         ])
         ->auth('user')
         ->rateLimit(10)
         ->register();
    /**
     * @param int $ID
     * @param string $content
     *
     * @return string
     */
    protected function getItemLink(int $ID, string $content):string
    {
        error_log('Type: '.print_r($content, true));
        switch ($content) {
            case BASE.'artist':
            case BASE.'artwork':
            case BASE.'event':
            case BASE.'news':
            case BASE.'offer':
            case BASE.'partner':
            case BASE.'piercing':
            case BASE.'tattoo':
                return get_permalink($ID);
            default:
                return get_term_link($ID, BASE.$content);
        }
    }
      // Mark as actioned
      Route::for('notifications/action')
         ->post([$this, 'markActioned'])
         ->args([
            'user' => 'integer|required',
            'notification_id' => 'integer|required',
         ])
         ->auth('user')
         ->rateLimit(30)
         ->register();
    /**
     * Get notification actions
     *
     * @param string $type Notification type
     * @param array $data Notification data
     * @param object $notification Full notification object
     *
     * @return array Actions available for this notification
     */
    protected function getNotificationActions(string $type, array $data, object $notification = null):array
    {
        error_log('Data for actions: '.print_r($data, true));
      // Dismiss notification
      Route::for('notifications/dismiss')
         ->post([$this, 'markDismissed'])
         ->args([
            'user' => 'integer|required',
            'notification_id' => 'integer|required',
         ])
         ->auth('user')
         ->rateLimit(30)
         ->register();
        $actions = [];
      // Get unread count
      Route::for('notifications/count')
         ->get([$this, 'getUnreadCount'])
         ->args([
            'user' => 'integer|required',
            'type' => 'string',
         ])
         ->auth('user')
         ->rateLimit()
         ->register();
   }
        switch ($type) {
            case 'artist_approved':
            case 'artist_rejected':
            case 'shop_approved':
            case 'shop_rejected':
            case 'term_approved':
            case 'term_rejected':
            case 'system_message':
                //No extra action needed
                break;
            case 'artist_invitation':
                $actions[] = [
                    'icon'      => 'upvote',
                    'label'     => 'Approve',
                    'action'    => 'acceptInvitation',
                ];
                $actions[] = [
                    'icon'      => 'downvote',
                    'label'     => 'Reject',
                    'action'    => 'reject_invitation',
                ];
                break;
            case 'artist_request':
                $actions[] = [
                    'icon'      => 'upvote',
                    'label'     => 'Approve',
                    'action'    => 'accept_to_shop',
                ];
                $actions[] = [
                    'icon'      => 'downvote',
                    'label'     => 'Reject',
                    'action'    => 'reject_to_shop',
                ];
                break;
            case 'artist_approval':
                $actions[] = [
                    'icon'      => 'upvote',
                    'label'     => 'Approve',
                    'action'    => 'accept_artist',
                ];
                $actions[] = [
                    'icon'      => 'downvote',
                    'label'     => 'Reject',
                    'action'    => 'reject_artist',
                ];
                break;
   // =========================================================================
   // GET OPERATIONS
   // =========================================================================
            case 'new_term':
                $actions[] = [
                    'icon'      => 'upvote',
                    'label'     => 'Approve',
                    'action'    => 'approve_term',
                ];
                $actions[] = [
                    'icon'      => 'downvote',
                    'label'     => 'Reject',
                    'action'    => 'reject_term',
                ];
                break;
   /**
    * Get notifications for a user
    */
   public function getNotifications(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $type = sanitize_text_field($request->get_param('type') ?? '');
      $status = sanitize_text_field($request->get_param('status') ?? '');
      $limit = absint($request->get_param('limit'));
      $offset = absint($request->get_param('offset'));
            case 'list_shared':
                if (!empty($data['list_id'])) {
                    $actions[] = [
                        'icon'      => 'list-heart',
                        'label'     => 'View List',
                        'url'       => home_url("/dash/favourites/{$data['list_id']}"),
                    ];
                }
                break;
            default:
                $actions[] = [
                    'icon'      => 'link',
                    'label'     => 'View',
                    'url'       => $this->getItemLink($data['target_id'], $data['target_type']),
                ];
                break;
        }
        $actions[] = [
            'icon'      => 'close',
            'label'     => 'Dismiss',
            'action'    => 'dismiss_notification'
        ];
        // Allow customization via filter
        return apply_filters('jvb_notification_actions', $actions, $type, $data, $notification);
    }
    /**
     * @param int $user_id
     * @param array $data
     *
     * @return array
     */
    protected function getSanitizedData(int $user_id, array $data):array
    {
        $status = (array_key_exists('status', $data)) ? $data['status'] : 'unread';
        $limit = (array_key_exists('limit', $data)) ? $data['limit'] : 20;
        $offset = (array_key_exists('page', $data)) ? $data['page'] : 1;
        $type = (array_key_exists('type', $data)) ? $data['type'] : 'all';
        // Validate and sanitize status
        $allowed_statuses = ['unread', 'read', 'actioned', 'dismissed', 'all'];
        if (!in_array($status, $allowed_statuses)) {
            $this->logError("Invalid notification status", [
                'status' => $status,
                'user' => $user_id
            ], 'warning');
            $status = 'unread'; // Default to unread if invalid
        }
        if (!in_array($type, array_keys($this->typeMap))) {
            $this->logError("Invalid notification type", [
                'type' => $type,
                'user' => $user_id
            ], 'warning');
            $type = 'all';
        }
        // Validate and sanitize limit and offset
        $limit = absint($limit);
        if ($limit <= 0 || $limit > 100) {
            $limit = 20; // Use reasonable default if invalid
        }
        $offset = absint($offset);
        if ($offset < 0) {
            $offset = 1;
        }
        $return = [
            'status'    => $status,
            'limit'     => $limit,
            'page'      => $offset,
            'type'      => $type
        ];
        if (array_key_exists('grouped', $data)) {
            $return['grouped'] = $data['grouped'];
        }
        return $return;
    }
    /**
     * Get notifications for a user
     *
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function getNotifications(WP_REST_Request $request): WP_REST_Response
    {
        $data = $request->get_params();
        $user_id = $data['user'];
      if (!$this->userCheck($user_id)) {
         $this->logError("Invalid user ID for notifications", ['user' => $user_id], 'warning');
         return new WP_REST_Response([
            'success' => false,
            'message' => 'User doesn\'t match. Are you a bot?'
         ]);
      if (!$this->checkUser($user_id)) {
         return $this->unauthorized();
      }
      // Check HTTP cache headers (includes notification types in timestamp check)
      $cache_check = $this->checkUserHeaders($request, $user_id, 'notifications');
      if ($cache_check) {
         return $cache_check;
      $cacheKey = compact('user_id', 'type', 'status', 'limit', 'offset');
      $result = $this->cache->remember($cacheKey, function() use ($user_id, $type, $status, $limit, $offset) {
         // Build where conditions
         $where = ['owner_id' => $user_id];
         if ($type) $where['type'] = $type;
         if ($status) $where['status'] = $status;
         $items = $this->notifications
            ->where($where)
            ->orderBy('created_at', 'DESC')
            ->limit($limit, $offset)
            ->getResults();
         $total = $this->notifications->where($where)->countResults();
         // Format notifications
         $formatted = array_map([$this, 'formatNotification'], $items);
         return [
            'items' => $formatted,
            'total' => $total,
            'has_more' => ($offset + $limit) < $total
         ];
      });
      return $this->success($result);
   }
   /**
    * Get unread notification count
    */
   public function getUnreadCount(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $type = sanitize_text_field($request->get_param('type') ?? '');
      if (!$this->checkUser($user_id)) {
         return $this->unauthorized();
      }
        // Step 1: Build status/order/filter params
        $params = $this->getSanitizedData($user_id, $data);
        $status = $params['status'];
        $limit = $params['limit'];
        $offset = $params['page'];
        $type = $params['type'];
      $cacheKey = compact('user_id', 'type');
        // Try cache first with validated parameters
        $cache_key = "user_{$user_id}_merged_notifications_{$status}_{$type}_{$limit}_{$offset}";
        $cached = $this->cache->get($cache_key);
        if ($cached) {
            $response = new WP_REST_Response($cached);
         return $this->addCacheHeaders($response);
        }
      $count = $this->cache->remember($cacheKey, function() use ($user_id, $type) {
         $where = ['owner_id' => $user_id, 'status' => 'unread'];
         if ($type) $where['type'] = $type;
        try {
            // Step 2: Get regular notifications
            $regular_notifications = $this->getRegularNotifications($user_id, $params);
         return $this->notifications->where($where)->countResults();
      });
            // Step 3: Get content notifications
            $content_notifications = $this->getContentNotifications($user_id, $status, $limit, $offset);
      return $this->success(['count' => $count]);
   }
            // Step 4: Get approval notifications
            $approval_notifications = $this->getApprovalNotifications($user_id, $status);
   /**
    * Get grouped notifications for a user
    */
   public function getGroupedNotifications(
      int $user_id,
      string $status,
      int $limit,
      int $offset,
      string $type
   ): array {
      $cacheKey = compact('user_id', 'status', 'limit', 'offset', 'type');
            // Step 5: Merge in order of created date
            $notifications = array_merge(
                $regular_notifications,
                $content_notifications,
                $approval_notifications
            );
      return $this->cache->remember($cacheKey, function() use ($user_id, $status, $limit, $offset, $type) {
         $time_window = '24 HOUR';
            usort($notifications, function ($a, $b) {
                $date_a = strtotime($a['created_at'] ?? $a['date'] ?? date('Y-m-d H:i:s'));
                $date_b = strtotime($b['created_at'] ?? $b['date'] ?? date('Y-m-d H:i:s'));
                return $date_b - $date_a; // Sort from newest to oldest
            });
         // Build type filter
         $typeFilter = '';
         $typeValues = [];
            // Apply pagination
            $total_count = count($notifications);
            $notifications = array_slice($notifications, 0, $limit);
         if ($type !== 'all' && isset($this->typeMap[$type])) {
            $types = $this->typeMap[$type];
            if (!empty($types)) {
               $placeholders = implode(',', array_fill(0, count($types), '%s'));
               $typeFilter = "AND type IN ({$placeholders})";
               $typeValues = $types;
            }
         }
            // Step 6: Return result
            $response = [
                'notifications' => $notifications,
                'pagination' => [
                    'total' => $total_count,
                    'page' => $offset,
                    'per_page' => $limit,
                    'pages' => ceil($total_count / $limit),
                    'has_more' => ($offset * $limit + count($notifications)) < $total_count
                ]
            ];
         // Build status filter
         $statusFilter = '';
         if ($status === 'read') {
            $statusFilter = "AND status IN ('read', 'actioned')";
         } elseif ($status !== 'all') {
            $statusFilter = "AND status = %s";
            $typeValues[] = $status;
         }
            // Cache the result
            $this->cache->set($cache_key, $response, 'notifications_' . $user_id);
            $response = new WP_REST_Response($response);
         return $this->addCacheHeaders($response);
        } catch (Exception $e) {
            $this->logError("Error retrieving notifications", [
                'user_id' => $user_id,
                'error' => $e->getMessage()
            ]);
            return new WP_REST_Response([
                'notifications' => [],
                'pagination' => [
                    'total' => 0,
                    'page' => $offset,
                    'per_page' => $limit,
                    'pages' => 0,
                    'has_more' => false
                ]
            ]);
        }
    }
    /**
     * Get regular notifications from the notifications table
     *
     * @param int $user_id User ID
     * @param array $params Filter parameters
     * @return array Array of formatted notifications
     */
    protected function getRegularNotifications(int $user_id, array $params): array
    {
        $status = $params['status'];
        $limit = $params['limit'];
        $offset = $params['page'];
        $type = $params['type'];
        // Try to get from cache first with validated parameters
        $cache_key = "user_{$user_id}_regular_notifications_{$status}_{$type}_{$limit}_{$offset}";
        $cached = $this->cache->get($cache_key);
        if ($cached) {
            return $cached;
        }
        global $wpdb;
        $notifications_table = $wpdb->prefix . BASE . 'notifications';
        // Build status condition
        $status_condition = "1=1";
        if ($status === 'unread') {
            $status_condition = "status = 'unread'";
        } elseif ($status === 'read') {
            $status_condition = "status IN ('read', 'actioned')";
        } elseif ($status !== 'all') {
            $status_condition = $wpdb->prepare("status = %s", $status);
        }
        // Build type condition
        $type_condition = "1=1";
        if ($type !== 'all' && isset($this->typeMap[$type])) {
            $types = $this->typeMap[$type];
            if (!empty($types)) {
                $placeholders = implode(',', array_fill(0, count($types), '%s'));
                $type_condition = $wpdb->prepare("type IN ($placeholders)", $types);
            }
        }
        // Get notifications
        $notifications = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT * FROM {$notifications_table}
             WHERE owner_id = %d AND {$status_condition} AND {$type_condition}
             ORDER BY created_at DESC",
                $user_id
            )
        );
        // Format notifications
        $formatted = [];
        foreach ($notifications as $notification) {
            $formatted[] = $this->formatNotification($notification);
        }
        // Cache the results
        $this->cache->set($cache_key, $formatted, 'notifications_' . $user_id);
        return $formatted;
    }
    /**
     * Get approval notifications from the approval_requests table
     *
     * @param int $user_id User ID
     * @param string $status Filter by status
     * @return array Array of formatted approval notifications
     */
    protected function getApprovalNotifications(int $user_id, string $status): array
    {
        // Try to get from cache first
        $cache_key = "user_{$user_id}_approval_notifications_{$status}";
        $cached = $this->cache->get($cache_key);
        if ($cached) {
            return $cached;
        }
        global $wpdb;
        $formatted = [];
        // Build status condition
        $status_condition = "1=1";
        if ($status === 'unread') {
            $status_condition = "a.status = 'pending'";
        } elseif ($status === 'read') {
            $status_condition = "a.status IN ('approved', 'rejected')";
        } elseif ($status !== 'all') {
            $status_condition = $wpdb->prepare("a.status = %s", $status);
        }
        if ($this->isVerifiedUser($user_id)) {
            $approvals = jvbApprovalTypes();
            foreach ($approvals as $type => $config) {
                $table = $wpdb->prefix.BASE.'approval_'.$type.'requests';
                $votes = $wpdb->prefix.BASE.'approval_'.$type.'votes';
                $approvals = $wpdb->get_results(
                    $wpdb->prepare(
                        "SELECT a.*,
                COALESCE(v.vote, 'none') as user_vote
                FROM {$table} a
                LEFT JOIN {$votes} v ON a.id = v.request_id AND v.user_id = %d
                WHERE a.user_id != %d
                AND {$status_condition}
                ORDER BY a.created_at DESC",
                        $user_id,
                        $user_id
                    )
                );
                // Now filter out requests created by the current user
                foreach ($approvals as $approval) {
                    $requested_by = json_decode($approval->requested_by, true);
                    // Skip if the current user is the requester
                    if (is_array($requested_by) && in_array($user_id, $requested_by)) {
                        continue;
                    }
                    $formatted[] = $this->formatApprovalNotification($approval);
                }
            }
        }
        // Cache the results
        $this->cache->set($cache_key, $formatted, 'approvals');
        return $formatted;
    }
    /**
     * Format an approval request as a notification
     *
     * @param object $approval Approval request object
     * @return array Formatted notification
     */
    protected function formatApprovalNotification(object $approval): array
    {
        $data = json_decode($approval->data ?? '{}', true);
        $type_labels = [
            'artist_approval' => 'Artist Verification',
            'term_suggestion' => 'Term Suggestion'
        ];
        $status_labels = [
            'pending' => 'Pending',
            'approved' => 'Approved',
            'rejected' => 'Rejected',
            'expired' => 'Expired'
        ];
        $icon = ($approval->type === 'artist_approval') ? 'artist' : 'style';
        $message = '';
        if ($approval->type === 'artist_approval') {
            if ($approval->requested_by == $approval->target_id) {
                $message = "Your artist verification is {$status_labels[$approval->status]}";
            } else {
                $name = isset($data['display_name']) ? $data['display_name'] : 'An artist';
                $message = "{$name} is requesting verification";
            }
        } elseif ($approval->type === 'term_suggestion') {
            $term_name = $data['term_name'] ?? 'A term';
            $taxonomy = $data['taxonomy'] ?? '';
            $taxonomy_name = str_replace(BASE, '', $taxonomy);
            if ($approval->requested_by == get_current_user_id()) {
                $message = "Your {$taxonomy_name} suggestion '{$term_name}' is {$status_labels[$approval->status]}";
            } else {
                $message = "New {$taxonomy_name} suggestion: '{$term_name}'";
            }
        }
        return [
            'id' => 'approval_' . $approval->id,
            'type' => $approval->type,
            'message' => $message,
            'created_at' => $approval->created_at,
            'status' => $approval->status,
            'requires_action' => ($approval->status === 'pending' && $approval->requested_by != get_current_user_id()),
            'action_taken' => !empty($approval->user_vote) && $approval->user_vote !== 'none',
            'icon' => $icon,
            'priority' => 'high',
            'target' => [
                'id' => $approval->target_id,
                'type' => $approval->target_type
            ],
            'context' => $data,
            'approval_data' => [
                'required_approvals' => $approval->required_approvals,
                'current_approvals' => $approval->current_approvals,
                'expires_at' => $approval->expires_at
            ]
        ];
    }
    /**
     * Determine which table a notification type belongs to
     *
     * @param string $notificationType The notification type
     * @return string The table name ('notifications' or 'content_notifications')
     */
    protected function getTableForNotificationType(string $notificationType): string
    {
        foreach ($this->notificationTableMap as $table => $types) {
            if (in_array($notificationType, $types)) {
                return $table;
            }
        }
        // Default to the main notifications table if type is unknown
        return 'notifications';
    }
    /**
     * Get grouped notifications for a user
     * @param int $user_id User ID
     * @param string $status notification status
     * @param int $limit number of notifications to fetch
     * @param int $offset page to fetch
     * @param string $type notification type to fetch
     * @return array Grouped notifications with pagination info
     */
    public function getGroupedNotifications(int $user_id, string $status, int $limit, int $offset, string $type):array
    {
        $cache_key = "user_{$user_id}_grouped_notifications_{$status}_{$type}_{$limit}_{$offset}";
        $cached = $this->cache->get($cache_key);
        if ($cached !== false) {
            return $cached;
        }
        global $wpdb;
        // Build status condition
        $status_condition = "1=1";
        if ($status === 'unread') {
            $status_condition = "status = 'unread'";
        } elseif ($status === 'read') {
            $status_condition = "status IN ('read', 'actioned')";
        } elseif ($status !== 'all') {
            $status_condition = $wpdb->prepare("status = %s", $status);
        }
        // Build type condition
        $type_condition = "1=1";
        if ($type !== 'all' && isset($this->typeMap[$type])) {
            $types = $this->typeMap[$type];
            if (!empty($types)) {
                $placeholders = implode(',', array_fill(0, count($types), '%s'));
                $type_condition = $wpdb->prepare("type IN ($placeholders)", $types);
            }
        }
        try {
            // Time window for grouping (e.g., last 24 hours)
            $time_window = '24 HOUR';
            $table = $wpdb->prefix.BASE.'notifications';
            // Count notifications by action_user_id and type
            $grouped_counts = $wpdb->get_results(
                $wpdb->prepare(
                    "SELECT
         // Get grouped notifications
         $grouped = $this->notifications->queryResults(
            "SELECT
                    action_user_id,
                    type,
                    COUNT(*) as count,
                    MAX(created_at) as latest_time,
                    MIN(id) as first_id,
                    GROUP_CONCAT(target_type) as target_types
                 FROM {$table}
                 FROM {table}
                 WHERE owner_id = %d
                 AND action_user_id IS NOT NULL
                 AND {$status_condition}
                 AND {$type_condition}
                 AND created_at > DATE_SUB(NOW(), INTERVAL {$time_window})
                 {$statusFilter}
                 {$typeFilter}
                 GROUP BY action_user_id, type
                 ORDER BY latest_time DESC
                 LIMIT %d OFFSET %d",
                    $user_id,
                    $limit,
                    $offset
                )
            );
            array_merge([$user_id], $typeValues, [$limit, $offset])
         );
            // Get total count for pagination
            $total_count = $wpdb->get_var(
                $wpdb->prepare(
                    "SELECT COUNT(DISTINCT CONCAT(action_user_id, '_', type))
                 FROM {$table}
         // Get total count
         $total = $this->notifications->queryVar(
            "SELECT COUNT(DISTINCT CONCAT(action_user_id, '_', type))
                 FROM {table}
                 WHERE owner_id = %d
                 AND action_user_id IS NOT NULL
                 AND {$status_condition}
                 AND {$type_condition}
                 AND created_at > DATE_SUB(NOW(), INTERVAL {$time_window})",
                    $user_id
                )
            );
                 AND created_at > DATE_SUB(NOW(), INTERVAL {$time_window})
                 {$statusFilter}
                 {$typeFilter}",
            array_merge([$user_id], $typeValues)
         );
            // Format the grouped notifications
            $formatted = [];
            foreach ($grouped_counts as $group) {
                // Get acting user name
                $acting_user_name = jvbGetUsername($group->action_user_id);
         // Format results
         $formatted = [];
         foreach ($grouped as $group) {
            if ($group->count > 1) {
               // Grouped notification
               $target_types = explode(',', $group->target_types);
               $formatted[] = [
                  'id' => 'group_' . $group->first_id,
                  'type' => $group->type,
                  'message' => $this->buildGroupedMessage(
                     jvbGetUsername($group->action_user_id),
                     $group->type,
                     $group->count,
                     $target_types
                  ),
                  'created_at' => $group->latest_time,
                  'is_grouped' => true,
                  'group_count' => $group->count,
               ];
            } else {
               // Single notification
               $notification = $this->notifications
                  ->where([
                     'owner_id' => $user_id,
                     'action_user_id' => $group->action_user_id,
                     'type' => $group->type
                  ])
                  ->orderBy('created_at', 'DESC')
                  ->first();
                if ($group->count > 1) {
                    // Get unique target types for better message formatting
                    $target_types = array_unique(explode(',', $group->target_types));
               if ($notification) {
                  $formatted[] = $this->formatNotification($notification);
               }
            }
         }
                    // Build a grouped notification
                    $message = $this->buildGroupedMessage(
                        $acting_user_name,
                        $group->type,
                        $group->count,
                        $target_types
                    );
         return [
            'notifications' => $formatted,
            'pagination' => [
               'total' => (int)$total,
               'page' => $offset,
               'per_page' => $limit,
               'pages' => ceil($total / $limit)
            ],
            'has_more' => ($offset + $limit) < $total
         ];
      });
   }
                    $formatted[] = [
                        'id' => 'group_' . $group->first_id,
                        'type' => $group->type,
                        'message' => $message,
                        'created_at' => $group->latest_time,
                        'timestamp' => strtotime($group->latest_time),
                        'status' => $status,
                        'icon' => $this->notification_types[$group->type]['icon'] ?? 'info',
                        'priority' => $this->notification_types[$group->type]['priority'] ?? 'normal',
                        'is_grouped' => true,
                        'group_count' => $group->count,
                        'acting_user' => [
                            'id' => $group->action_user_id,
                            'name' => $acting_user_name
                        ],
                        'target_types' => $target_types
                    ];
                } else {
                    // Get the single notification details
                    $table = $wpdb->prefix.BASE.'notifications';
                    $notification = $wpdb->get_row(
                        $wpdb->prepare(
                            "SELECT * FROM {$table}
                         WHERE owner_id = %d
                         AND action_user_id = %d
                         AND type = %s
                         ORDER BY created_at DESC
                         LIMIT 1",
                            $user_id,
                            $group->action_user_id,
                            $group->type
                        )
                    );
   /**
    * Get regular notifications from the notifications table
    */
   protected function getRegularNotifications(int $user_id, array $params): array
   {
      $cacheKey = compact('user_id', 'params');
                    if ($notification) {
                        $formatted[] = $this->formatNotification($notification);
                    }
                }
            }
      return $this->cache->remember($cacheKey, function() use ($user_id, $params) {
         $status = $params['status'];
         $type = $params['type'];
         $limit = $params['limit'];
         $offset = $params['page'];
            // Prepare response with pagination info
            $response = [
                'notifications' => $formatted,
                'pagination' => [
                    'total' => (int)$total_count,
                    'page' => $offset,
                    'per_page' => $limit,
                    'pages' => ceil($total_count / $limit),
                    'has_more' => ($offset + $limit) < $total_count
                ]
            ];
         // Build base query
         $where = ['owner_id' => $user_id];
            // Cache the results
            $this->cache->set($cache_key, $response, 'notifications_' . $user_id);
         // Handle status filter
         if ($status === 'read') {
            // For multiple statuses, use raw query
            $notifications = $this->getNotificationsWithMultipleStatuses(
               $user_id,
               ['read', 'actioned'],
               $type,
               $limit,
               $offset
            );
         } else {
            // Single status - use fluent builder
            if ($status !== 'all') {
               $where['status'] = $status;
            }
            return $response;
        } catch (Exception $e) {
            $this->logError("Error retrieving grouped notifications", [
                'user_id' => $user_id,
                'status' => $status,
                'error' => $e->getMessage()
            ]);
            // Handle type filter
            if ($type !== 'all' && isset($this->typeMap[$type])) {
               $types = $this->typeMap[$type];
               if (!empty($types)) {
                  // Multiple types - use raw query
                  return $this->getNotificationsByTypes(
                     $user_id,
                     $types,
                     $status,
                     $limit,
                     $offset
                  );
               }
            }
            return [
                'notifications' => [],
                'pagination' => [
                    'total' => 0,
                    'page' => $offset,
                    'per_page' => $limit,
                    'pages' => 0,
                    'has_more' => false
                ]
            ];
        }
    }
            // Simple query - use fluent builder
            $notifications = $this->notifications
               ->where($where)
               ->orderBy('created_at', 'DESC')
               ->limit($limit, $offset)
               ->getResults();
         }
    /**
     * Build a message for grouped notifications
     *
     * @param string $user_name Acting user's name
     * @param string $type Notification type
     * @param int $count Number of grouped notifications
     * @param array $target_types Types of targets involved
     * @return string Formatted message
     */
    protected function buildGroupedMessage(string $user_name, string $type, int $count, array $target_types = []):string
    {
        switch ($type) {
            case 'new_favourite':
                // If we have a single target type
                if (count($target_types) === 1) {
                    $content_type = $this->manager->getContentTypeLabel($target_types[0]);
                    return "{$user_name} favourited {$count} of your {$content_type}";
                }
                return "{$user_name} favourited {$count} of your items";
         // Format notifications
         return array_map([$this, 'formatNotification'], $notifications);
      });
   }
            case 'artist_request':
                return "{$user_name} wants to join your shop";
   /**
    * Get notifications with multiple status values
    */
   protected function getNotificationsWithMultipleStatuses(
      int $user_id,
      array $statuses,
      string $type,
      int $limit,
      int $offset
   ): array {
      $placeholders = implode(',', array_fill(0, count($statuses), '%s'));
      $params = array_merge([$user_id], $statuses);
            // Add more cases for other notification types
            default:
                return "{$user_name} has {$count} notifications for you";
        }
    }
      $typeCondition = '';
      if ($type !== 'all' && isset($this->typeMap[$type])) {
         $types = $this->typeMap[$type];
         if (!empty($types)) {
            $typePlaceholders = implode(',', array_fill(0, count($types), '%s'));
            $typeCondition = "AND type IN ({$typePlaceholders})";
            $params = array_merge($params, $types);
         }
      }
      $params[] = $limit;
      $params[] = $offset;
    /**
     * Build Notification request
     *
     * @param WP_REST_Request $request
     * @return array Sanitized parameters for checking the cache
     */
    protected function buildParams(WP_REST_Request $request):array
    {
        $request = $request->get_params();
        return [
            'status'        => (array_key_exists('status', $request) && in_array($request['status'], ['all', 'unread', 'expired'])) ? $request['status'] : 'unread',
            'user_id'       => (array_key_exists('user', $request) && is_int($request['user'])) ? $request['user'] : get_current_user_id(),
            'page'          => (array_key_exists('page', $request) && is_numeric($request['page'])) ? $request['page'] : 1,
            'type'          => (array_key_exists('type', $request) && in_array($request['type'], array_keys($this->manager->notification_types))) ? $request['type'] : 'all',
        ];
    }
      return $this->notifications->queryResults(
         "SELECT * FROM {table}
             WHERE owner_id = %d
             AND status IN ({$placeholders})
             {$typeCondition}
             ORDER BY created_at DESC
             LIMIT %d OFFSET %d",
         $params
      );
   }
    /**
     * @param WP_REST_Request $request
     *
     * @return WP_REST_Response
     */
    public function updateNotifications(WP_REST_Request $request):WP_REST_Response
    {
        $data = $request->get_params();
        $action = $request->get_param('action');
        $notificationID = (array_key_exists('notification', $data) && is_int($data['notification'])) ? $data['notification'] : false;
        $args = $this->buildParams($request);
        $data = [];
        $error = '';
        switch ($action) {
            case 'mark_as_read':
                if (is_null($notificationID)) {
                    return new WP_REST_Response([
                        'success'   => false,
                        'message'   => 'Notification ID is required'
                    ]);
                }
                $data = [
                    'user_id'   => $args['user_id'],
                    'notification_id' => $notificationID,
                ];
                break;
            case 'mark_all_as_read':
                $data = [
                    'user_id'   => $args['user_id'],
                ];
                if ($request->get_param('notification_ids')) {
                    //To bulk mark all ids
                    $data['notification_ids'] = array_map('intval', $request->get_param('notification_ids'));
                }
                if ($request->get_param('type')) {
                    //To bulk select all items by type
                    $data['type'] = $request->get_param('type');
                }
                break;
            case 'approve_artist':
            case 'reject_artist':
                //TODO: hook into the approval routes already set up
                $handler = JVB()->routes('approvals');
                $ID = $handler->getVerificationDetails($args['user_id']);
                $notes = isset($request['notes']) ? sanitize_text_field($request['notes']) : '';
                $handler->voteForArtist($args['user_id'], $ID['request'], $action==='approve_artist', $notes);
   /**
    * Get notifications by multiple types
    */
   protected function getNotificationsByTypes(
      int $user_id,
      array $types,
      string $status,
      int $limit,
      int $offset
   ): array {
      $placeholders = implode(',', array_fill(0, count($types), '%s'));
      $params = array_merge([$user_id], $types, [$limit, $offset]);
                $this->markActioned($notificationID, $args['user_id']);
                break;
            case 'acceptInvitation':
            case 'reject_invitation':
                //TODO: hook into the shop routes already set up
                $handler = JVB()->routes('shop');
                $notification_data = $this->getNotification($notificationID);
      $statusCondition = '';
      if ($status !== 'all') {
         $statusCondition = "AND status = %s";
         array_splice($params, -2, 0, [$status]);
      }
                if ($action === 'acceptInvitation') {
                    $result = $handler->acceptShopInvitation($args['user_id'], $args['shop_id']);
                } else {
                    $result = $handler->declineShopInvitation($args['user_id'], $args['shop_id']);
                }
      return $this->notifications->queryResults(
         "SELECT * FROM {table}
             WHERE owner_id = %d
             AND type IN ({$placeholders})
             {$statusCondition}
             ORDER BY created_at DESC
             LIMIT %d OFFSET %d",
         $params
      );
   }
                $this->markActioned($notificationID, $args['user_id']);
                break;
            case 'accept_to_shop':
            case 'reject_to_shop':
                $handler = JVB()->routes('shop');
   /**
    * Get approval notifications from the approval_requests table
    */
   protected function getApprovalNotifications(int $user_id, string $status): array
   {
      $cacheKey = compact('user_id', 'status');
                if ($action === 'accept_to_shop') {
                    $result = $handler->addArtistToShop($args['user_id'], $args['shop_id']);
                } else {
                    //TODO: notify requester that their request has been denied
                }
      return $this->cache->remember($cacheKey, function() use ($user_id, $status) {
         if (!$this->isVerifiedUser($user_id)) {
            return [];
         }
                $this->markActioned($notificationID, $args['user_id']);
                break;
            case 'approve_term':
            case 'reject_term':
                $handler = JVB()->routes('approvals');
                $notification_data = $this->getNotification($notificationID);
                $notes = isset($request['notes']) ? sanitize_text_field($request['notes']) : '';
         global $wpdb;
         $formatted = [];
                if ($action === 'approve_term') {
                    $result = $handler->approveTerm($args['user_id'], $notification_data, $notes);
                } else {
                    $result = $handler->rejectTerm($args['user_id'], $notification_data, $notes);
                }
         // Build status condition
         $statusCondition = "1=1";
         if ($status === 'unread') {
            $statusCondition = "a.status = 'pending'";
         } elseif ($status === 'read') {
            $statusCondition = "a.status IN ('approved', 'rejected')";
         } elseif ($status !== 'all') {
            $statusCondition = $wpdb->prepare("a.status = %s", $status);
         }
                $this->markActioned($notificationID, $args['user_id']);
                break;
            case 'dismiss_notification':
                $data = [
                    'user_id'   => $args['user_id'],
                    'notification_id' => $notificationID,
                ];
                break;
         $approvals = Registrar::getFeatured('approve_new');
         foreach ($approvals as $type => $config) {
            $table = $wpdb->prefix . BASE . 'approval_' . $type . 'requests';
            $votes = $wpdb->prefix . BASE . 'approval_' . $type . 'votes';
            default:
                $error = 'Invalid action';
            $approvalRequests = $wpdb->get_results(
               $wpdb->prepare(
                  "SELECT a.*,
                        COALESCE(v.vote, 'none') as user_vote
                        FROM {$table} a
                        LEFT JOIN {$votes} v ON a.id = v.request_id AND v.user_id = %d
                        WHERE a.user_id != %d
                        AND {$statusCondition}
                        ORDER BY a.created_at DESC",
                  $user_id,
                  $user_id
               )
            );
        }
            // Filter out requests created by current user
            foreach ($approvalRequests as $approval) {
               $requested_by = json_decode($approval->requested_by, true);
        if (!empty($data)) {
            JVB()->queue()->queueOperation(
                'notification_'.$action,
                $args['user_id'],
                $data
            );
            return new WP_REST_Response([
                'success'   => true,
                'message'   => __('Notification queued for processing', 'jvb')
            ]);
        } else {
            return new WP_REST_Response([
                'success'   => false,
                'message'   => __('Error: '.$error, 'jvb')
            ]);
        }
    }
               if (is_array($requested_by) && in_array($user_id, $requested_by)) {
                  continue;
               }
    /**
     * Get notification data by ID
     * @param int $notification_id Notification ID
     * @return array|false Notification data or false if not found
     */
    protected function getNotification(int $notification_id):array|false
    {
        global $wpdb;
        $table = $wpdb->prefix.BASE.'notifications';
        $notification = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM {$table} WHERE id = %d",
                $notification_id
            ),
            ARRAY_A
        );
               $formatted[] = $this->formatApprovalNotification($approval);
            }
         }
        if (!$notification) {
            return false;
        }
         return $formatted;
      });
   }
        // Parse context data
        $context = !empty($notification['context'])
            ? json_decode($notification['context'], true)
            : [];
   /**
    * Get content notifications for a user
    */
   public function getContentNotifications(
      int $user_id,
      string $status = 'unread',
      int $limit = 20,
      int $offset = 0
   ): array {
      if (!$this->checkUser($user_id)) {
         return [];
      }
        return array_merge($notification, $context);
    }
      $cacheKey = compact('user_id', 'status', 'limit', 'offset');
    /**
     * @param WP_Error|array $result
     * @param object $operation
     * @param array $data
     *
     * @return int|mixed
     */
    public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array
    {
        switch ($operation->type) {
            case 'notification_mark_as_read':
                $result = $this->markRead($data['notification_id'], $operation->user_id);
                break;
            case 'notification_mark_all_as_read':
                $result = $this->markAllRead($data);
                break;
            case 'notification_dismiss_notification':
                $result = $this->markDismissed($data['notification_id'], $operation->user_id);
                break;
        }
        return $result;
    }
      return $this->cache->remember($cacheKey, function() use ($user_id, $status, $limit, $offset) {
         global $wpdb;
    /**
     * Mark all notifications as read for a user
     *
     * @param array $data Data containing user_id and optional filters
     *
     * @return array  Number of notifications marked as read
     */
    public function markAllRead(array $data):array
    {
        $user_id = $data['user_id'];
        $type = $data['type'] ?? null;
        $notification_ids = $data['notification_ids'] ?? null;
         // Build status condition
         $statusCondition = "1=1";
         if ($status === 'unread') {
            $statusCondition = "seen.status = 'unread'";
         } elseif ($status === 'read') {
            $statusCondition = "seen.status = 'read'";
         } elseif ($status !== 'all') {
            $statusCondition = $wpdb->prepare("seen.status = %s", $status);
         }
        global $wpdb;
        $table = $wpdb->prefix . BASE . 'notifications';
         $notifications = $wpdb->get_results(
            $wpdb->prepare(
               "SELECT seen.*, content.*
                     FROM {$wpdb->prefix}" . BASE . "notifications_user_seen AS seen
                     JOIN {$wpdb->prefix}" . BASE . "notifications_content AS content
                         ON seen.content_notification_id = content.id
                     WHERE seen.user_id = %d AND {$statusCondition}
                     ORDER BY content.date DESC, content.created_at DESC
                     LIMIT %d OFFSET %d",
               $user_id,
               $limit,
               $offset
            )
         );
        $where = [
            'owner_id = %d',
            "status = 'unread'"
        ];
        $params = [$user_id];
         // Format content notifications
         return array_map([$this, 'formatContentNotification'], $notifications);
      });
   }
        // Add type filter if specified
        if ($type) {
            $where[] = "type = %s";
            $params[] = $type;
        }
   // =========================================================================
   // UPDATE OPERATIONS
   // =========================================================================
        // Add specific IDs filter if provided
        if ($notification_ids && is_array($notification_ids)) {
            $id_placeholders = implode(',', array_fill(0, count($notification_ids), '%d'));
            $where[] = "id IN ($id_placeholders)";
            $params = array_merge($params, $notification_ids);
        }
   /**
    * Mark notification as read
    */
   public function markRead(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $notification_id = absint($request->get_param('notification_id'));
        $where_clause = implode(' AND ', $where);
      if (!$this->checkUser($user_id)) {
         return $this->unauthorized();
      }
        // Update all matching notifications
        $updated = $wpdb->query($wpdb->prepare(
            "UPDATE $table
         SET status = 'read',
             read_at = %s,
             updated_at = %s
         WHERE $where_clause",
            array_merge(
                [current_time('mysql'), current_time('mysql')],
                $params
            )
        ));
      try {
         $result = $this->notifications->transaction(function($table) use ($notification_id, $user_id) {
            // Verify ownership
            $notification = $table
               ->where(['id' => $notification_id, 'owner_id' => $user_id])
               ->first();
        if ($updated) {
            $this->trackNotificationMetrics(0, $user_id, 'batch_read', ['count' => $updated]);
            $this->clearNotificationCache($user_id);
        }
            if (!$notification) {
               throw new Exception('Invalid notification');
            }
        return [
            'success'   => true,
            'result'   => $updated
        ];
    }
            $updated = $table->update(
               [
                  'status' => 'read',
                  'read_at' => current_time('mysql')
               ],
               ['id' => $notification_id]
            );
    /**
     * Mark a notification as read
     *
     * @param int $notification_id Notification ID
     * @param int $user_id User ID making the request (for security)
     *
     * @return array Success or failure
     */
    public function markRead(int $notification_id, int $user_id):array
    {
        if (!$this->checkUser($user_id)) {
            return [
                'success'   => false,
                'result'   => 'Invalid user'
            ];
        }
        global $wpdb;
            if (!$updated) {
               throw new Exception('Failed to update notification');
            }
            return $updated;
         });
         $this->trackMetrics($notification_id, $user_id, 'read');
         $this->clearUserCache($user_id);
         return $this->success(['updated' => $result]);
      } catch (Exception $e) {
         return $this->error($e->getMessage());
      }
   }
   /**
    * Mark all notifications as read
    */
   public function markAllRead(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $type = sanitize_text_field($request->get_param('type') ?? '');
      if (!$this->checkUser($user_id)) {
         return $this->unauthorized();
      }
      try {
         $where = ['owner_id' => $user_id, 'status' => 'unread'];
         if ($type) $where['type'] = $type;
         $updated = $this->notifications
            ->where($where)
            ->updateResults([
               'status' => 'read',
               'read_at' => current_time('mysql')
            ]);
         if ($updated) {
            $this->trackMetrics(0, $user_id, 'batch_read', ['count' => $updated]);
            $this->clearUserCache($user_id);
         }
         return $this->success(['updated' => $updated]);
      } catch (Exception $e) {
         return $this->error($e->getMessage());
      }
   }
   /**
    * Mark notification as actioned
    */
   public function markActioned(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $notification_id = absint($request->get_param('notification_id'));
      if (!$this->checkUser($user_id)) {
         return $this->unauthorized();
      }
      try {
         $result = $this->notifications->transaction(function($table) use ($notification_id, $user_id) {
            // Verify ownership and requires action
            $notification = $table
               ->where(['id' => $notification_id, 'owner_id' => $user_id, 'requires_action' => 1])
               ->first();
            if (!$notification) {
               throw new Exception('Invalid notification or does not require action');
            }
            $updated = $table->update(
               [
                  'status' => 'actioned',
                  'action_taken' => 1,
                  'actioned_at' => current_time('mysql')
               ],
               ['id' => $notification_id]
            );
            if (!$updated) {
               throw new Exception('Failed to update notification');
            }
            return $updated;
         });
         $this->trackMetrics($notification_id, $user_id, 'actioned');
         $this->clearUserCache($user_id);
         return $this->success(['message' => 'Notification actioned']);
      } catch (Exception $e) {
         return $this->error($e->getMessage());
      }
   }
   /**
    * Dismiss notification
    */
   public function markDismissed(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $notification_id = absint($request->get_param('notification_id'));
      if (!$this->checkUser($user_id)) {
         return $this->unauthorized();
      }
      try {
         $result = $this->notifications->transaction(function($table) use ($notification_id, $user_id) {
            // Verify ownership
            $notification = $table
               ->where(['id' => $notification_id, 'owner_id' => $user_id])
               ->first();
            if (!$notification) {
               throw new Exception('Invalid notification');
            }
            $updated = $table->update(
               [
                  'status' => 'dismissed',
                  'dismissed_at' => current_time('mysql')
               ],
               ['id' => $notification_id]
            );
            if (!$updated) {
               throw new Exception('Failed to update notification');
            }
            return $updated;
         });
         $this->trackMetrics($notification_id, $user_id, 'dismissed');
         $this->clearUserCache($user_id);
         return $this->success(['updated' => $result]);
      } catch (Exception $e) {
         return $this->error($e->getMessage());
      }
   }
   /**
    * Mark content notification as read
    */
   public function markContentNotificationRead(int $seen_id, int $user_id): array
   {
      if (!$this->checkUser($seen_id) || !$this->checkUser($user_id)) {
         return [
            'success' => false,
            'message' => 'Invalid User ID'
         ];
      }
      try {
         global $wpdb;
         $table = $wpdb->prefix . BASE . 'notifications_user_seen';
         return $wpdb->query('START TRANSACTION') &&
            $this->updateContentNotificationStatus($table, $seen_id, $user_id, 'read');
      } catch (Exception $e) {
         global $wpdb;
         $wpdb->query('ROLLBACK');
         $this->logError('markContentNotificationRead exception', [
            'seen_id' => $seen_id,
            'user_id' => $user_id,
            'error' => $e->getMessage()
         ]);
         return [
            'success' => false,
            'message' => $e->getMessage()
         ];
      }
   }
   /**
    * Dismiss content notification
    */
   public function dismissContentNotification(int $seen_id, int $user_id): array
   {
      if (!$this->checkUser($seen_id) || !$this->checkUser($user_id)) {
         return [
            'success' => false,
            'message' => 'Invalid User ID'
         ];
      }
      try {
         global $wpdb;
         $table = $wpdb->prefix . BASE . 'notifications_user_seen';
         // Verify ownership
         $seen_record = $wpdb->get_row(
            $wpdb->prepare(
               "SELECT * FROM {$table} WHERE id = %d AND user_id = %d",
               $seen_id,
               $user_id
            )
         );
         if (!$seen_record) {
            return [
               'success' => false,
               'message' => 'No notification found'
            ];
         }
         $result = $wpdb->update(
            $table,
            ['status' => 'dismissed'],
            ['id' => $seen_id]
         );
         if ($result !== false) {
            $this->clearUserCache($user_id);
         }
         return [
            'success' => $result !== false,
            'message' => 'Operation completed',
         ];
      } catch (Exception $e) {
         return [
            'success' => false,
            'message' => $e->getMessage()
         ];
      }
   }
   // =========================================================================
   // QUEUE OPERATIONS
   // =========================================================================
   /**
    * Process queued notification operations
    */
   public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
   {
      switch ($operation->type) {
         case 'notification_mark_as_read':
            $result = $this->markReadQueued($data['notification_id'], $operation->user_id);
            break;
         case 'notification_mark_all_as_read':
            $result = $this->markAllReadQueued($data);
            break;
         case 'notification_dismiss_notification':
            $result = $this->markDismissedQueued($data['notification_id'], $operation->user_id);
            break;
      }
      return $result;
   }
   /**
    * Update notification operations (legacy endpoint)
    */
   public function updateNotifications(WP_REST_Request $request): WP_REST_Response
   {
      $data = $request->get_params();
      $action = $request->get_param('action');
      $notificationID = absint($data['notification'] ?? 0);
      $args = $this->buildParams($request);
      $queueData = [];
      $error = '';
      switch ($action) {
         case 'mark_as_read':
            if (!$notificationID) {
               return $this->validationError(['notification' => 'Notification ID is required']);
            }
            $queueData = [
               'user_id' => $args['user_id'],
               'notification_id' => $notificationID,
            ];
            break;
         case 'mark_all_as_read':
            $queueData = ['user_id' => $args['user_id']];
            if ($request->get_param('notification_ids')) {
               $queueData['notification_ids'] = array_map('intval', $request->get_param('notification_ids'));
            }
            if ($request->get_param('type')) {
               $queueData['type'] = $request->get_param('type');
            }
            break;
         case 'dismiss_notification':
            $queueData = [
               'user_id' => $args['user_id'],
               'notification_id' => $notificationID,
            ];
            break;
         default:
            $error = 'Invalid action';
      }
      if (!empty($queueData)) {
         JVB()->queue()->queueOperation(
            'notification_' . $action,
            $args['user_id'],
            $queueData
         );
         return $this->success(['message' => __('Notification queued for processing', 'jvb')]);
      }
      return $this->error($error ?: 'Unknown error');
   }
   // =========================================================================
   // FORMATTING HELPERS
   // =========================================================================
   /**
    * Format a notification for display
    */
   protected function formatNotification(object $notification): array
   {
      $config = $this->notification_types[$notification->type] ?? [];
      $context = json_decode($notification->context ?? '{}', true);
      // Get action user's name if available
      $acting_user_name = null;
      if ($notification->action_user_id) {
         $acting_user_name = jvbShareName($notification->action_user_id);
      }
        //TODO: We need to set up a system to check the main notification table, but also the content notification table
        // Verify ownership
        $table = $wpdb->prefix . BASE . 'notifications';
        $notification = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM {$table}
             WHERE id = %d AND owner_id = %d",
                $notification_id,
                $user_id
            )
        );
      return [
         'id' => $notification->id,
         'type' => $notification->type,
         'message' => $notification->message,
         'created_at' => $notification->created_at,
         'status' => $notification->status,
         'requires_action' => (bool)$notification->requires_action,
         'action_taken' => (bool)$notification->action_taken,
         'icon' => $config['icon'] ?? 'info',
         'priority' => $notification->priority,
         'target' => [
            'id' => $notification->target_id,
            'type' => $notification->target_type
         ],
         'context' => $context,
         'acting_user' => $notification->action_user_id ? [
            'id' => $notification->action_user_id,
            'name' => $acting_user_name
         ] : null,
         'actions' => $this->getNotificationActions($notification->type, (array)$notification, $notification)
      ];
   }
        if (!$notification) {
            return [
                'success'   => false,
                'result'   => 'Invalid notification'
            ];
        }
   /**
    * Format an approval request as a notification
    */
   protected function formatApprovalNotification(object $approval): array
   {
      $data = json_decode($approval->data ?? '{}', true);
      $type_labels = [
         'artist_approval' => 'Artist Verification',
         'term_suggestion' => 'Term Suggestion'
      ];
        // Mark as read
        $result = $wpdb->update(
            $wpdb->prefix . BASE . 'notifications',
            [
                'status' => 'read',
                'read_at' => current_time('mysql'),
                'updated_at' => current_time('mysql')
            ],
            ['id' => $notification_id]
        );
      $status_labels = [
         'pending' => 'Pending',
         'approved' => 'Approved',
         'rejected' => 'Rejected',
         'expired' => 'Expired'
      ];
        if ($result) {
            $this->clearNotificationCache($user_id);
        }
      $icon = ($approval->type === 'artist_approval') ? 'artist' : 'style';
        return [
            'success'   => $result !== false,
            'result'   => $result
        ];
    }
      $message = '';
      if ($approval->type === 'artist_approval') {
         if ($approval->requested_by == $approval->target_id) {
            $message = "Your artist verification is {$status_labels[$approval->status]}";
         } else {
            $name = $data['display_name'] ?? 'An artist';
            $message = "{$name} is requesting verification";
         }
      } elseif ($approval->type === 'term_suggestion') {
         $term_name = $data['term_name'] ?? 'A term';
         $taxonomy = $data['taxonomy'] ?? '';
         $taxonomy_name = str_replace(BASE, '', $taxonomy);
    /**
     * Mark a notification as actioned
     *
     * @param int $notification_id Notification ID
     * @param int $user_id User ID making the request
     *
     * @return array
     */
    public function markActioned(int $notification_id, int $user_id):array
    {
        if (!$this->checkUser($user_id)) {
            return [
                'success'   => false,
                'message'   => 'Invalid user'
            ];
        }
        global $wpdb;
         if ($approval->requested_by == get_current_user_id()) {
            $message = "Your {$taxonomy_name} suggestion '{$term_name}' is {$status_labels[$approval->status]}";
         } else {
            $message = "New {$taxonomy_name} suggestion: '{$term_name}'";
         }
      }
        //TODO: We need to set up a system to check the main notification table, but also the content notification table
        // Start a transaction
        $wpdb->query('START TRANSACTION');
      return [
         'id' => 'approval_' . $approval->id,
         'type' => $approval->type,
         'message' => $message,
         'created_at' => $approval->created_at,
         'status' => $approval->status,
         'requires_action' => ($approval->status === 'pending' && $approval->requested_by != get_current_user_id()),
         'action_taken' => !empty($approval->user_vote) && $approval->user_vote !== 'none',
         'icon' => $icon,
         'priority' => 'high',
         'target' => [
            'id' => $approval->target_id,
            'type' => $approval->target_type
         ],
         'context' => $data,
         'approval_data' => [
            'required_approvals' => $approval->required_approvals,
            'current_approvals' => $approval->current_approvals,
            'expires_at' => $approval->expires_at
         ]
      ];
   }
        try {
            $table = $wpdb->prefix.BASE.'notifications';
            // Verify ownership and that notification requires action
            $notification = $wpdb->get_row(
                $wpdb->prepare(
                    "SELECT * FROM {$table}
                 WHERE id = %d AND user_id = %d AND requires_action = 1
                 FOR UPDATE", // Lock the row
                    $notification_id,
                    $user_id
                )
            );
   /**
    * Format a content notification for display
    */
   protected function formatContentNotification(object $notification): array
   {
      // Get artist data
      $artist_data = jvbContentFromUser($notification->user_id);
            if (!$notification) {
                $wpdb->query('ROLLBACK');
                $this->logError("Failed to action notification - not found or not owned", [
                    'notification_id' => $notification_id,
                    'user_id' => $user_id
                ], 'warning');
                return [
                    'success'   => false,
                    'message'   => 'No notification found, or invalid owner'
                ];
            }
      // Parse JSON data
      $new_items = json_decode($notification->new_items, true) ?: [];
      $updated_items = json_decode($notification->updated_items, true) ?: [];
            // Mark as actioned
            $result = $wpdb->update(
                $wpdb->prefix . BASE . 'notifications',
                [
                    'status' => 'actioned',
                    'action_taken' => 1,
                    'actioned_at' => current_time('mysql'),
                    'updated_at' => current_time('mysql')
                ],
                ['id' => $notification_id]
            );
      // Count items by type
      $counts_by_type = [];
      $total_new = 0;
            if ($result !== false) {
                $wpdb->query('COMMIT');
                $this->clearNotificationCache($user_id);
                return [
                    'success'   => true,
                    'message'   => 'Notification actioned'
                ];
            } else {
                $wpdb->query('ROLLBACK');
                $this->logError("Database error marking notification as actioned", [
                    'notification_id' => $notification_id,
                    'user_id' => $user_id,
                    'db_error' => $wpdb->last_error
                ]);
                return [
                    'success'   => false,
                    'message'   => 'Error'
                ];
            }
        } catch (Exception $e) {
            $wpdb->query('ROLLBACK');
            $this->logError("Exception marking notification as actioned", [
                'notification_id' => $notification_id,
                'user_id' => $user_id,
                'error' => $e->getMessage()
            ]);
            return [
                'success'   => false,
                'message'   => $e->getMessage(),
            ];
        }
    }
      foreach ($new_items as $type => $ids) {
         $clean_type = str_replace(BASE, '', $type);
         $counts_by_type[$clean_type] = [
            'new' => count($ids),
            'updated' => 0
         ];
         $total_new += count($ids);
      }
    /**
     * Dismiss a notification
     *
     * @param int $notification_id Notification ID
     * @param int $user_id User ID making the request
     *
     * @return array Success or failure
     */
    public function markDismissed(int $notification_id, int $user_id):array{
        if (!$this->checkUser($user_id)) {
            return [
                'success'   => false,
                'result'   => 'Invalid User',
            ];
        }
        global $wpdb;
      foreach ($updated_items as $type => $ids) {
         $clean_type = str_replace(BASE, '', $type);
         if (!isset($counts_by_type[$clean_type])) {
            $counts_by_type[$clean_type] = ['new' => 0];
         }
         $counts_by_type[$clean_type]['updated'] = count($ids);
      }
        // Verify ownership
        $table = $wpdb->prefix.BASE.'notifications';
        $notification = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM {$table}
                 WHERE id = %d AND user_id = %d",
                $notification_id,
                $user_id
            )
        );
//TODO: We need to set up a system to check the main notification table, but also the content notification table
        if (!$notification) {
            return [
                'success'   => false,
                'result'   => 'Invalid notification'
            ];
        }
      // Build summary text
      $summary = [];
      foreach ($counts_by_type as $type => $counts) {
         if ($counts['new'] > 0) {
            $label = $counts['new'] === 1 ? $type : $this->pluralize($type);
            $summary[] = "{$counts['new']} new {$label}";
         }
         if ($counts['updated'] > 0) {
            $label = $counts['updated'] === 1 ? $type : $this->pluralize($type);
            $summary[] = "{$counts['updated']} updated {$label}";
         }
      }
        // Mark as dismissed
        $result = $wpdb->update(
            $wpdb->prefix . BASE . 'notifications',
            [
                'status'     => 'dismissed',
                'updated_at' => current_time('mysql')
            ],
            [ 'id' => $notification_id ]
        );
      return [
         'id' => $notification->id,
         'seen_id' => $notification->content_notification_id,
         'status' => $notification->status,
         'date' => $notification->date,
         'artist' => $artist_data,
         'new_items' => $new_items,
         'updated_items' => $updated_items,
         'summary' => implode(', ', $summary),
         'total_new' => $total_new,
         'total_updated' => $notification->total_items - $total_new,
         'counts_by_type' => $counts_by_type,
         'has_profile_update' => (bool)$notification->has_profile_update,
         'created_at' => $notification->created_at
      ];
   }
        if ($result) {
            $this->clearNotificationCache($user_id);
        }
   /**
    * Build a message for grouped notifications
    */
   protected function buildGroupedMessage(string $user_name, string $type, int $count, array $target_types = []): string
   {
      switch ($type) {
         case 'new_favourite':
            if (count($target_types) === 1) {
               $content_type = $this->manager->getContentTypeLabel($target_types[0]);
               return "{$user_name} favourited {$count} of your {$content_type}";
            }
            return "{$user_name} favourited {$count} of your items";
        return [
            'success'   => $result !== false,
            'result'   => $result
        ];
    }
         case 'artist_request':
            return "{$user_name} wants to join your shop";
    /***
     * Content notifications (in a separate database))
     */
    /**
     * Get content notifications for a user
     *
     * @param int $user_id User ID
     * @param string $status Status filter (unread, read, all)
     * @param int $limit Maximum number of notifications to return
     * @param int $offset Pagination offset
     *
     * @return array Content notifications
     */
    public function getContentNotifications(
        int $user_id,
        string $status = 'unread',
        int $limit = 20,
        int $offset = 0
    ):array {
        if (!$this->checkUser($user_id)) {
            return [];
        }
        // Try cache first
        $cache_key = "user_{$user_id}_content_notifications_{$status}_{$limit}_{$offset}";
        $cached    = $this->cache->get($cache_key);
         default:
            return "{$user_name} has {$count} notifications for you";
      }
   }
        if ($cached !== false) {
            return $cached;
        }
   // =========================================================================
   // HELPER METHODS
   // =========================================================================
        global $wpdb;
   /**
    * Get notification actions
    */
   protected function getNotificationActions(string $type, array $data, ?object $notification = null): array
   {
      $actions = [];
        // Build status condition
        $status_condition = "1=1";
        if ($status === 'unread') {
            $status_condition = "seen.status = 'unread'";
        } elseif ($status === 'read') {
            $status_condition = "seen.status = 'read'";
        } elseif ($status !== 'all') {
            $status_condition = $wpdb->prepare("seen.status = %s", $status);
        }
      switch ($type) {
         case 'artist_approved':
         case 'artist_rejected':
         case 'shop_approved':
         case 'shop_rejected':
         case 'term_approved':
         case 'term_rejected':
         case 'system_message':
            // No extra action needed
            break;
        // Get content notifications this user has seen records for
         case 'artist_invitation':
            $actions[] = [
               'icon' => 'upvote',
               'label' => 'Approve',
               'action' => 'acceptInvitation',
            ];
            $actions[] = [
               'icon' => 'downvote',
               'label' => 'Reject',
               'action' => 'reject_invitation',
            ];
            break;
        $notifications = $wpdb->get_results(
            $wpdb->prepare(
                "SELECT seen.*, content.*
                 FROM {$wpdb->prefix}{$this->base}notifications_user_seen AS seen
                 JOIN {$wpdb->prefix}{$this->base}notifications_content AS content
                     ON seen.content_notification_id = content.id
                 WHERE seen.user_id = %d AND {$status_condition}
                 ORDER BY content.date DESC, content.created_at DESC
                 LIMIT %d OFFSET %d",
                $user_id,
                $limit,
                $offset
            )
        );
         case 'artist_request':
            $actions[] = [
               'icon' => 'upvote',
               'label' => 'Approve',
               'action' => 'accept_to_shop',
            ];
            $actions[] = [
               'icon' => 'downvote',
               'label' => 'Reject',
               'action' => 'reject_to_shop',
            ];
            break;
        // Format content notifications
        $formatted = [];
        foreach ($notifications as $notification) {
            $formatted[] = $this->formatContentNotification($notification);
        }
         case 'new_term':
            $actions[] = [
               'icon' => 'upvote',
               'label' => 'Approve',
               'action' => 'approve_term',
            ];
            $actions[] = [
               'icon' => 'downvote',
               'label' => 'Reject',
               'action' => 'reject_term',
            ];
            break;
        // Cache the results
        $this->cache->set($cache_key, $formatted, 'notifications_' . $user_id);
         case 'list_shared':
            if (!empty($data['list_id'])) {
               $actions[] = [
                  'icon' => 'list-heart',
                  'label' => 'View List',
                  'url' => home_url("/dash/favourites/{$data['list_id']}"),
               ];
            }
            break;
        return $formatted;
    }
         default:
            if (!empty($data['target_id']) && !empty($data['target_type'])) {
               $actions[] = [
                  'icon' => 'link',
                  'label' => 'View',
                  'url' => $this->getItemLink($data['target_id'], $data['target_type']),
               ];
            }
            break;
      }
    /**
     * Format a content notification for display
     *
     * @param object $notification Content notification object
     *
     * @return array Formatted content notification
     */
    protected function formatContentNotification(object $notification):array
    {
        // Get artist data
        $artist_data = jvbContentFromUser($notification->user_id);
      $actions[] = [
         'icon' => 'close',
         'label' => 'Dismiss',
         'action' => 'dismiss_notification'
      ];
        // Parse JSON data
        $new_items     = json_decode($notification->new_items, true) ?: [];
        $updated_items = json_decode($notification->updated_items, true) ?: [];
      return apply_filters('jvb_notification_actions', $actions, $type, $data, $notification);
   }
        // Count items by type
        $counts_by_type = [];
        $total_new      = 0;
   /**
    * Get item link for notification target
    */
   protected function getItemLink(int $ID, string $content): string
   {
      switch ($content) {
         case BASE.'artist':
         case BASE.'artwork':
         case BASE.'event':
         case BASE.'news':
         case BASE.'offer':
         case BASE.'partner':
         case BASE.'piercing':
         case BASE.'tattoo':
            return get_permalink($ID);
         default:
            return get_term_link($ID, BASE.$content);
      }
   }
        foreach ($new_items as $type => $ids) {
            $clean_type                    = str_replace(BASE, '', $type);
            $counts_by_type[ $clean_type ] = [
                'new'     => count($ids),
                'updated' => 0
            ];
            $total_new += count($ids);
        }
   /**
    * Get notification data by ID
    */
   protected function getNotification(int $notification_id): array|false
   {
      $notification = $this->notifications->where(['id' => $notification_id])->first();
        foreach ($updated_items as $type => $ids) {
            $clean_type = str_replace(BASE, '', $type);
            if (!isset($counts_by_type[ $clean_type ])) {
                $counts_by_type[ $clean_type ] = [
                    'new'     => 0
                ];
            }
            $counts_by_type[ $clean_type ]['updated'] = count($ids);
        }
      if (!$notification) {
         return false;
      }
        // Build summary text
        $summary = [];
        foreach ($counts_by_type as $type => $counts) {
            if ($counts['new'] > 0) {
                $label = $counts['new'] === 1 ? $type : $this->pluralize($type);
                $summary[] = "{$counts['new']} new {$label}";
            }
            if ($counts['updated'] > 0) {
                $label     = $counts['updated'] === 1 ? $type : $this->pluralize($type);
                $summary[] = "{$counts['updated']} updated {$label}";
            }
        }
      $context = !empty($notification->context)
         ? json_decode($notification->context, true)
         : [];
        return [
            'id'                 => $notification->id,
            'seen_id'            => $notification->content_notification_id,
            'status'             => $notification->status,
            'date'               => $notification->date,
            'artist'             => $artist_data,
            'new_items'          => $new_items,
            'updated_items'      => $updated_items,
            'summary'            => implode(', ', $summary),
            'total_new'          => $total_new,
            'total_updated'      => $notification->total_items - $total_new,
            'counts_by_type'     => $counts_by_type,
            'has_profile_update' => (bool) $notification->has_profile_update,
            'created_at'         => $notification->created_at
        ];
    }
      return array_merge((array)$notification, $context);
   }
    /**
     * Mark a content notification as read
     *
     * @param int $seen_id User seen record ID
     * @param int $user_id User ID making the request
     *
     * @return array Success or failure
     */
    public function markContentNotificationRead(int $seen_id, int $user_id):array
    {
        // Validate input parameters
        if (!$this->checkUser($seen_id)) {
            $this->logError("Invalid seen ID", [
                'seen_id' => $seen_id,
                'user_id' => $user_id
            ], 'warning');
            return [
                'success'   => false,
                'message'   => 'Invalid User ID'
            ];
        }
   /**
    * Build notification request parameters
    */
   protected function buildParams(WP_REST_Request $request): array
   {
      $params = $request->get_params();
        if (!$this->checkUser($user_id)) {
            $this->logError("Invalid user ID", [
                'seen_id' => $seen_id,
                'user_id' => $user_id
            ], 'warning');
            return [
                'success'   => false,
                'message'   => 'Invalid User ID'
            ];
        }
      return [
         'status' => in_array($params['status'] ?? '', ['all', 'unread', 'expired'])
            ? $params['status']
            : 'unread',
         'user_id' => absint($params['user'] ?? get_current_user_id()),
         'page' => absint($params['page'] ?? 1),
         'type' => in_array($params['type'] ?? '', array_keys($this->manager->notification_types ?? []))
            ? $params['type']
            : 'all',
      ];
   }
        global $wpdb;
        $table = $wpdb->prefix . BASE . 'notifications_user_seen';
   /**
    * Determine which table a notification type belongs to
    */
   protected function getTableForNotificationType(string $notificationType): string
   {
      foreach ($this->notificationTableMap as $table => $types) {
         if (in_array($notificationType, $types)) {
            return $table;
         }
      }
      return 'notifications';
   }
        // Start transaction
        $wpdb->query('START TRANSACTION');
   /**
    * Simple pluralization helper
    */
   protected function pluralize(string $word): string
   {
      $irregular = [
         'tattoo' => 'tattoos',
         'piercing' => 'piercings',
         'artwork' => 'artwork',
         'news' => 'news',
         'offer' => 'offers',
         'event' => 'events'
      ];
        try {
            // Verify ownership with row locking
            $seen_record = $wpdb->get_row(
                $wpdb->prepare(
                    "SELECT * FROM {$table}
                 WHERE id = %d AND user_id = %d
                 FOR UPDATE",
                    $seen_id,
                    $user_id
                )
            );
      if (isset($irregular[$word])) {
         return $irregular[$word];
      }
            if (!$seen_record) {
                $wpdb->query('ROLLBACK');
                $this->logError("Seen record not found or not owned by user", [
                    'seen_id' => $seen_id,
                    'user_id' => $user_id
                ], 'warning');
                return [
                    'success'   => false,
                    'message'   => 'Seen record not found or not owned by user'
                ];
            }
      if (substr($word, -1) === 'y') {
         return substr($word, 0, -1) . 'ies';
      }
            // Mark as read
            $result = $wpdb->update(
                $table,
                [
                    'status' => 'read',
                    'read_at' => current_time('mysql')
                ],
                ['id' => $seen_id]
            );
      return $word . 's';
   }
            if ($result !== false) {
                $wpdb->query('COMMIT');
                $this->clearNotificationCache($user_id);
                return [
                    'success'   => true,
                    'message'   => 'Successfully marked as seen'
                ];
            } else {
                $wpdb->query('ROLLBACK');
                $this->logError("Database error marking content notification as read", [
                    'seen_id' => $seen_id,
                    'user_id' => $user_id,
                    'db_error' => $wpdb->last_error
                ]);
                return [
                    'success'   => false,
                    'message'   => 'Something went wrong...'
                ];
            }
        } catch (Exception $e) {
            $wpdb->query('ROLLBACK');
            $this->logError("Exception marking content notification as read", [
                'seen_id' => $seen_id,
                'user_id' => $user_id,
                'error' => $e->getMessage()
            ]);
            return [
                'success'   => false,
                'message'   => $e->getMessage()
            ];
        }
    }
   /**
    * Track notification metrics
    */
   protected function trackMetrics(int $notification_id, int $user_id, string $action, array $details = []): void
   {
      try {
         $this->metrics->create([
            'notification_id' => $notification_id,
            'user_id' => $user_id,
            'action' => $action,
            'action_source' => 'web',
            'action_details' => !empty($details) ? json_encode($details) : null,
         ]);
      } catch (Exception $e) {
         // Don't fail the request if metrics tracking fails
         $this->logError('trackMetrics failed', [
            'error' => $e->getMessage(),
            'user_id' => $user_id,
            'action' => $action
         ]);
      }
   }
   /**
    * Clear notification cache for a user
    */
   protected function clearUserCache(int $user_id): void
   {
      Cache::invalidateItem('notifications', $user_id);
   }
    /**
     * Dismiss a content notification
     *
     * @param int $seen_id User seen record ID
     * @param int $user_id User ID making the request
     *
     * @return array Success or failure
     */
    public function dismissContentNotification( int $seen_id, int $user_id ):array
    {
        // Validate input parameters
        if (!$this->checkUser($seen_id)) {
            $this->logError("Invalid seen ID", [
                'seen_id' => $seen_id,
                'user_id' => $user_id
            ], 'warning');
            return [
                'success'   => false,
                'message'   => 'Invalid User ID'
            ];
        }
   /**
    * Update content notification status (helper for transactions)
    */
   private function updateContentNotificationStatus(string $table, int $seen_id, int $user_id, string $status): array
   {
      global $wpdb;
        if (!$this->checkUser($user_id)) {
            $this->logError("Invalid user ID", [
                'seen_id' => $seen_id,
                'user_id' => $user_id
            ], 'warning');
            return [
                'success'   => false,
                'message'   => 'Invalid User ID'
            ];
        }
        global $wpdb;
      // Verify ownership with row locking
      $seen_record = $wpdb->get_row(
         $wpdb->prepare(
            "SELECT * FROM {$table} WHERE id = %d AND user_id = %d FOR UPDATE",
            $seen_id,
            $user_id
         )
      );
        // Verify ownership
        $seen_record = $wpdb->get_row(
            $wpdb->prepare(
                "SELECT * FROM {$wpdb->prefix}{$this->base}notifications_user_seen
                 WHERE id = %d AND user_id = %d",
                $seen_id,
                $user_id
            )
        );
      if (!$seen_record) {
         $wpdb->query('ROLLBACK');
         return [
            'success' => false,
            'message' => 'Seen record not found or not owned by user'
         ];
      }
        if (!$seen_record) {
            return [
                'success'   => false,
                'message'   => 'No notification found'
            ];
        }
      // Update status
      $result = $wpdb->update(
         $table,
         [
            'status' => $status,
            'read_at' => current_time('mysql')
         ],
         ['id' => $seen_id]
      );
        // Mark as dismissed
        $result = $wpdb->update(
            $wpdb->prefix . BASE . 'notifications_user_seen',
            [
                'status' => 'dismissed'
            ],
            [ 'id' => $seen_id ]
        );
      if ($result !== false) {
         $wpdb->query('COMMIT');
         $this->clearUserCache($user_id);
         return [
            'success' => true,
            'message' => 'Successfully marked as ' . $status
         ];
      }
        if ($result) {
            $this->clearNotificationCache($user_id);
        }
      $wpdb->query('ROLLBACK');
      return [
         'success' => false,
         'message' => 'Database error'
      ];
   }
        return [
            'success' => $result !== false,
            'message'   => 'Operation completed',
        ];
    }
   /**
    * Queued operation helpers (for backwards compatibility)
    */
   private function markReadQueued(int $notification_id, int $user_id): array
   {
      $updated = $this->notifications->update(
         ['status' => 'read', 'read_at' => current_time('mysql')],
         ['id' => $notification_id, 'owner_id' => $user_id]
      );
    /**
     * Simple pluralization helper
     *
     * @param string $word Word to pluralize
     * @return string Pluralized word
     */
    protected function pluralize(string $word): string
    {
        $irregular = [
            'tattoo' => 'tattoos',
            'piercing' => 'piercings',
            'artwork' => 'artwork',
            'news' => 'news',
            'offer' => 'offers',
            'event' => 'events'
        ];
      if ($updated) {
         $this->clearUserCache($user_id);
      }
        if (isset($irregular[$word])) {
            return $irregular[$word];
        }
      return ['success' => $updated !== false];
   }
        // Simple pluralization rules
        if (substr($word, -1) === 'y') {
            return substr($word, 0, -1) . 'ies';
        }
   private function markAllReadQueued(array $data): array
   {
      $where = ['owner_id' => $data['user_id'], 'status' => 'unread'];
      if (!empty($data['type'])) {
         $where['type'] = $data['type'];
      }
        return $word . 's';
    }
    /**
     * Track notification metrics for analytics
     *
     * @param int $notification_id Notification ID (0 for batch operations)
     * @param int $user_id User ID
     * @param string $action Action taken (read, dismiss, etc.)
     * @param array $details Additional details
     * @return bool Success or failure
     */
    protected function trackNotificationMetrics(int $notification_id, int $user_id, string $action, array $details = []): bool
    {
        global $wpdb;
        $metrics_table = $wpdb->prefix . BASE . 'notification_metrics';
      $updated = $this->notifications
         ->where($where)
         ->updateResults(['status' => 'read', 'read_at' => current_time('mysql')]);
        try {
            return $wpdb->insert(
                $metrics_table,
                [
                    'notification_id' => $notification_id,
                    'user_id' => $user_id,
                    'action' => $action,
                    'action_source' => 'web',
                    'action_details' => json_encode($details),
                    'created_at' => current_time('mysql')
                ]
            ) !== false;
        } catch (Exception $e) {
            $this->logError("Failed to track notification metrics", [
                'user_id' => $user_id,
                'action' => $action,
                'error' => $e->getMessage()
            ]);
            return false;
        }
    }
      if ($updated) {
         $this->clearUserCache($data['user_id']);
      }
    /**
     * Clear notification cache for a user, for all notification types
     *
     * @param int $user_id User ID
     * @return void
     */
    protected function clearNotificationCache(int $user_id): void
    {
        // Clear regular notifications cache
        $this->cache->invalidate("user_{$user_id}_notifications_");
        $this->cache->invalidate("user_{$user_id}_regular_notifications_");
      return ['success' => $updated !== false, 'count' => $updated];
   }
        // Clear content notifications cache
        $this->cache->invalidate("user_{$user_id}_content_notifications_");
   private function markDismissedQueued(int $notification_id, int $user_id): array
   {
      $updated = $this->notifications->update(
         ['status' => 'dismissed', 'dismissed_at' => current_time('mysql')],
         ['id' => $notification_id, 'owner_id' => $user_id]
      );
        // Clear approval notifications cache
        $this->cache->invalidate("user_{$user_id}_approval_notifications_");
      if ($updated) {
         $this->clearUserCache($user_id);
      }
        // Clear merged notifications cache
        $this->cache->invalidate("user_{$user_id}_merged_notifications_");
    }
      return ['success' => $updated !== false];
   }
}