Jake Vanderwerf
2026-03-03 772462eeca3002a1d52508aeba485aab2b4742ad
inc/rest/routes/FavouritesRoutes.php
@@ -1,3472 +1,2116 @@
<?php
namespace JVBase\rest\routes;
use JVBase\JVB;
use JVBase\rest\RestRouteManager;
use JVBase\managers\CacheManager;
use JVBase\managers\Cache;
use JVBase\managers\CustomTable;
use JVBase\registrar\Registrar;
use JVBase\rest\PermissionHandler;
use JVBase\rest\Response;
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;
}
class FavouritesRoutes extends RestRouteManager
/**
 * TODO: Extract business logic into a Favourites.php manager class
 */
class FavouritesRoutes extends Rest
{
    protected array $valid_types;
    protected int $user_id;
   protected array $valid_types;
   protected Cache $listsCache;
   protected Cache $sharedListsCache;
   protected Cache $favouritesCache;
   protected CustomTable $favourites;
   protected CustomTable $lists;
   protected CustomTable $listItems;
   protected CustomTable $listShares;
    public function __construct()
    {
        $this->cache_name = 'favourites';
        parent::__construct();
        $this->valid_types = array_keys(array_merge(JVB_CONTENT, JVB_TAXONOMY));
        $this->user_id = get_current_user_id();
        $this->action = 'favourites-';
        add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
        add_action('before_delete_post', [$this, 'cleanupPostFavourites']);
        add_action('delete_term', [$this, 'cleanupTermFavourites'], 10, 3);
        // Register cleanup scheduler
        add_action('jvb_cleanupOrphanedFavourites', [$this, 'cleanupOrphanedFavourites']);
    }
    /**
     * Registers favourites routes
     * @return void
     */
    public function registerRoutes():void
    {
        // Main favourites endpoint - GET for retrieval, POST for toggling/notes
        register_rest_route($this->namespace, '/favourites', [
            [
                'methods' => 'GET',
                'callback' => [$this, 'getFavourites'],
                'permission_callback' => [$this, 'checkPermission']
            ],
            [
                'methods' => 'POST',
                'callback' => [$this, 'handleFavouriteOperation'],
                'permission_callback' => [$this, 'checkPermission']
            ]
        ]);
        // Lists endpoint - GET for retrieval, POST for creation/modifications
        register_rest_route($this->namespace, '/favourites/lists', [
            [
                'methods' => 'GET',
                'callback' => [$this, 'getLists'],
                'permission_callback' => [$this, 'checkPermission']
            ],
            //Adding and removing list items is handled by the body dta
            [
                'methods' => 'POST',
                'callback' => [$this, 'handleListOperation'],
                'permission_callback' => [$this, 'checkPermission']
            ]
        ]);
        // List shares operations
        register_rest_route($this->namespace, '/favourites/lists/shares', [
            [
                'methods' => 'GET',
                'callback' => [$this, 'getShares'],
                'permission_callback' => [$this, 'checkPermission']
            ],
            //Adds and removes are handled in the body data
            [
                'methods' => 'POST',
                'callback' => [$this, 'handleShare'],
                'permission_callback' => [$this, 'checkPermission']
            ]
        ]);
    }
   protected function buildParams(WP_REST_Request $request):array
   public function __construct()
   {
      $data = $request->get_params();
      error_log('Favourites Request Data: '.print_r($data, true));
      $args = [];
      if (!array_key_exists('user', $data)) {
         return $args;
      }
      $args['user'] = absint($data['user']);
      if (!array_key_exists('page', $data)) {
         //No filters set, just get a list of favourites
         return $args;
      }
      $args = array_merge($args, [
         'page'         => max(1, absint($data['page'] ?? 1)),
         'content'      => $this->checkContent($data['content'])
      ]);
      return $this->applyOrderFilters($args, $data);
      $this->cacheName = 'favourites';
      $this->cacheTtl = HOUR_IN_SECONDS;
      parent::__construct();
      // Set up cache connections
      $this->cache->connect('post')->connect('user')->connect('taxonomy');
      $this->listsCache = Cache::for('lists')->connect('favourites', true);
      $this->sharedListsCache = Cache::for('sharedLists')->connect('favourites', true);
      $this->favouritesCache = Cache::for('allFavourites')->connect('favourites', true);
      $this->valid_types = array_merge(Registrar::getRegistered('post'), Registrar::getRegistered('term'));
      // Initialize CustomTable instances
      $this->favourites = CustomTable::for('favourites');
      $this->lists = CustomTable::for('favourites_lists');
      $this->listItems = CustomTable::for('favourites_list_items');
      $this->listShares = CustomTable::for('favourites_list_shares');
      // Register hooks
      add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
      add_action('before_delete_post', [$this, 'cleanupPostFavourites']);
      add_action('delete_term', [$this, 'cleanupTermFavourites'], 10, 3);
      add_action('jvbUserRegistered', [$this, 'maybeAcceptListInvite'], 10, 3);
      add_action('jvb_cleanupOrphanedFavourites', [$this, 'cleanupOrphanedFavourites']);
   }
    /**
     * Get user's favourites with optional filtering
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response Response with favourites data
     */
    public function getFavourites(WP_REST_Request $request):WP_REST_Response
    {
      $args = $this->buildParams($request);
      if (!$args['user'] || $args['user'] === ''){
         $result = [
            'success'   => false,
            'message'   => 'No user set'
         ];
   public function registerRoutes(): void
   {
      // Favourites endpoints
      Route::for('favourites')
         ->get([$this, 'getFavourites'])
         ->args([
            'user' => 'integer|required',
            'type' => 'string',
            'include_all' => 'boolean',
         ])
         ->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]))
         ->rateLimit(30)
         ->post([$this, 'handleFavourite'])
         ->args([
            'user' => 'integer|required',
            'id' => 'string|required',
            'action' => 'string|required|enum:add,remove,toggle,batch,note',
            'type' => 'string',
            'target_id' => 'integer',
            'items' => 'array',
            'notes' => 'string',
         ])
         ->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]))
         ->rateLimit(30)
         ->register();
      // Lists endpoints
      Route::for('favourites/lists')
         ->get([$this, 'getLists'])
         ->args(['user' => 'integer|required'])
         ->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]))
         ->rateLimit(30)
         ->post([$this, 'handleList'])
         ->args([
            'user' => 'integer|required',
            'id' => 'string|required',
            'action' => 'string|required|enum:create,update,delete,share,unshare,add_items,remove_items',
            'list_id' => 'integer',
            'name' => 'string',
            'items' => 'array',
         ])
         ->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]))
         ->rateLimit(20)
         ->register();
      // Favourite counts
      Route::for('favourites/counts')
         ->get([$this, 'getFavouriteCounts'])
         ->args(['user' => 'integer|required'])
         ->auth(PermissionHandler::combine(['user', ['actionNonce' => 'favourites-']]))
         ->register();
   }
   /**
    * Get user's favourites with optional filtering
    */
   public function getFavourites(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      if (!$this->userCheck($user_id)) {
         return $this->unauthorized();
      }
      // Check HTTP cache headers for user-specific data
      $cache_check = $this->checkUserHeaders($request, $args['user'], 'favourites');
      $args = $this->buildParams($request);
      $key = $this->cache->generateKey($args);
      // Check cache headers
      $cache_check = $this->checkHeaders($request, $key);
      if ($cache_check) {
         return $cache_check;
      }
      if (count($args) === 1 || (array_key_exists('all', $args) && $args['all'] === true)) {
            $result = $this->getAllFavourites($args['user']);
      if (count($args) === 1 || ($request->get_param('include_all') === true)) {
         $result = $this->getAllFavourites($user_id);
      } else {
         $result = $this->cache->remember(
            $args,
            function() use ($args) {
               $response = new WP_REST_Response($this->getFilteredFavourites($args));
               return $this->addCacheHeaders($response);
            }
         );
         $result = $this->cache->remember($key, function() use ($args) {
            return $this->getFilteredFavourites($args);
         });
      }
      $response = new WP_REST_Response($result);
      return $this->addCacheHeaders($response);
    }
   protected function getFilteredFavourites(array $args):array
      return $this->addCacheHeaders(Response::success($result));
   }
   /**
    * Get filtered favourites using CustomTable fluent interface
    */
   protected function getFilteredFavourites(array $args): array
   {
      try {
         global $wpdb;
         $table = $wpdb->prefix . BASE . 'favourites';
         // Build query with proper escaping
         $query_parts = ["SELECT f.* FROM {$table} f WHERE user_id = %d"];
         $params = [$args['user']];
         // Build base query
         $query = $this->favourites->where(['user_id' => $args['user']]);
         // Add type filter if specified
         if ($args['content'] && $args['content']!=='all') {
            $query_parts[] = "AND type = %s";
            $params[] = BASE . $args['content'];
         if (!empty($args['content']) && $args['content'] !== 'all') {
            $query = $this->favourites->where([
               'user_id' => $args['user'],
               'type' => BASE . $args['content']
            ]);
         }
         // Add ordering - make sure to use whitelist for column names
         if ($args['orderby'] === 'date') {
            $args['orderby'] = 'date_added';
         // Apply ordering and pagination
         $orderby = in_array($args['orderby'] ?? 'date_added', ['date_added', 'type'])
            ? $args['orderby']
            : 'date_added';
         $order = in_array(strtoupper($args['order'] ?? 'DESC'), ['ASC', 'DESC'])
            ? strtoupper($args['order'])
            : 'DESC';
         $favourites = $query
            ->orderBy($orderby, $order)
            ->limit(100, ($args['page'] - 1) * 100)
            ->getResults();
         // Get total count
         $count_query = $this->favourites->where(['user_id' => $args['user']]);
         if (!empty($args['content']) && $args['content'] !== 'all') {
            $count_query->where(['type' => BASE . $args['content']]);
         }
         $valid_orderby_columns = ['date_added', 'type'];
         $valid_orders = ['ASC', 'DESC'];
         $orderby = in_array($args['orderby'], $valid_orderby_columns) ? $args['orderby'] : 'date_added';
         $order = in_array(strtoupper($args['order']), $valid_orders) ? strtoupper($args['order']) : 'DESC';
         $query_parts[] = "ORDER BY {$orderby} {$order}";
         $total_items = $count_query->countResults();
         // Add pagination
         $query_parts[] = "LIMIT %d OFFSET %d";
         $params[] = 100;
         $params[] = ($args['page'] - 1) * 100;
         // Execute query
         $query = implode(' ', $query_parts);
         $favourites = $wpdb->get_results($wpdb->prepare($query, $params));
         // Get total count for pagination
         $count_query = "SELECT COUNT(*) FROM {$table} WHERE user_id = %d";
         $count_params = [$args['user']];
         if ($args['content'] && $args['content'] !== 'all') {
            $count_query .= " AND type = %s";
            $count_params[] = BASE . $args['content'];
         }
         $total_items = (int)$wpdb->get_var($wpdb->prepare($count_query, $count_params));
         // Format the favourites using batch processing to reduce queries
         $formatted = $this->formatItems($favourites);
         // Get counts by type for filters
         $counts = $this->getFavouriteCounts($args['user']);
         // Prepare response data
         return [
            'items'     => $formatted,
            'has_more'  => ($args['page'] * 100) < $total_items,
            'total'     => $total_items,
            'success'      => true,
            'items' => $this->formatItems($favourites),
            'has_more' => ($args['page'] * 100) < $total_items,
            'total' => $total_items,
            'success' => true,
         ];
      } catch (Exception $e) {
         $this->logError(
            $e->getMessage(),
            [
               'method' => 'getFilteredFavourites',
               'args'      => $args
            ]
         );
         $this->logError('getFilteredFavourites', [
            'error' => $e->getMessage(),
            'args' => $args
         ]);
         return [
            'success'   => false,
            'favourites'   => [],
            'counts'    => 0,
            'pagination'   => []
            'success' => false,
            'items' => [],
            'total' => 0,
            'has_more' => false
         ];
      }
   }
    /**
     * Get all user's favourites organized by content type
     *
     * @param int $user_id User ID
     * @return WP_REST_Response Response with favourites by content type
     */
    protected function getAllFavourites(int $user_id):WP_REST_Response
    {
        if (!$this->checkUser($user_id)) {
            return new WP_REST_Response([
                'success'   => false,
                'message'   => 'User ID doesn\'t match... are you a bot?'
            ]);
        }
      $result = $this->cache->remember(
         'user_'.$user_id.'_all_favourites',
         function() use ($user_id) {
            return $this->fetchAllFavourites($user_id);
         }
      );
      return new WP_REST_Response($result);
    }
   protected function fetchAllFavourites(int $user_id):array
   /**
    * Get all user's favourites organized by content type
    */
   protected function getAllFavourites(int $user_id): array
   {
      try {
         global $wpdb;
         $table = $wpdb->prefix . BASE . 'favourites';
      return $this->cache->remember($user_id, function() use ($user_id) {
         try {
            $favourites = $this->favourites
               ->where(['user_id' => $user_id])
               ->getResults();
         // Get all favourites for this user
         $query = $wpdb->prepare(
            "SELECT type, target_id FROM {$table} WHERE user_id = %d",
            $user_id
         );
         $favourites = $wpdb->get_results($query);
         // Organize by content type
         $by_type = [];
         foreach ($favourites as $fav) {
            $type = str_replace(BASE, '', $fav->type);
            if (!isset($by_type[$type])) {
               $by_type[$type] = [];
            $by_type = [];
            foreach ($favourites as $fav) {
               $type = str_replace(BASE, '', $fav->type);
               if (!isset($by_type[$type])) {
                  $by_type[$type] = [];
               }
               $by_type[$type][] = (int)$fav->target_id;
            }
            $by_type[$type][] = (int)$fav->target_id;
            return [
               'success' => true,
               'items' => $by_type,
               'has_more' => false,
            ];
         } catch (Exception $e) {
            $this->logError('getAllFavourites', [
               'error' => $e->getMessage(),
               'user_id' => $user_id
            ]);
            return [
               'success' => false,
               'items' => [],
            ];
         }
      });
   }
   /**
    * Handle favourite operations
    */
   public function handleFavourite(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $operation_id = sanitize_text_field($request->get_param('id'));
      $action = sanitize_text_field($request->get_param('action'));
      if (!$this->userCheck($user_id)) {
         return $this->unauthorized();
      }
      $data = [
         'action' => $action,
         'type' => sanitize_text_field($request->get_param('type') ?? ''),
         'target_id' => absint($request->get_param('target_id') ?? 0),
         'items' => $request->get_param('items') ?? [],
         'notes' => sanitize_textarea_field($request->get_param('notes') ?? ''),
      ];
      JVB()->queue()->queueOperation(
         'favourite_' . $action,
         $user_id,
         $data,
         [
            'operation_id' => $operation_id,
            'priority' => 'high',
         ]
      );
      return $this->queued($operation_id);
   }
   /**
    * Get user's favourite lists
    */
   public function getLists(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      if (!$this->userCheck($user_id)) {
         return $this->unauthorized();
      }
      $params = ['user' => $user_id];
      if ($request->get_param('id')) {
         $params['list'] = sanitize_text_field($request->get_param('id'));
      }
      $key = $this->listsCache->generateKey($params);
      // Check cache headers
      $cache_check = $this->checkHeaders($request, $key);
      if ($cache_check) {
         return $cache_check;
      }
      $list_id = $request->get_param('id');
      $response = $list_id
         ? $this->getListDetails($list_id, $user_id)
         : $this->getAvailableLists($user_id);
      return $this->addCacheHeaders(Response::success($response));
   }
   /**
    * Get lists available to a user using CustomTable
    */
   protected function getAvailableLists(int $user_id, bool $include_shared = true): array
   {
      if (!$this->checkUser($user_id)) {
         return [];
      }
      $cache = $include_shared ? $this->sharedListsCache : $this->listsCache;
      return $cache->remember($user_id, function() use ($user_id, $include_shared) {
         try {
            // Get owned lists
            $owned = $this->lists
               ->where(['user_id' => $user_id])
               ->orderBy('created_at', 'DESC')
               ->getResults(ARRAY_A);
            // Add item counts
            foreach ($owned as &$list) {
               $list['item_count'] = $this->listItems
                  ->where(['list_id' => $list['id']])
                  ->countResults();
               $list['is_owner'] = true;
               $list['is_shared'] = false;
            }
            if (!$include_shared) {
               return [
                  'success' => true,
                  'lists' => $owned
               ];
            }
            // Get shared lists
            $shares = $this->listShares
               ->where(['user_id' => $user_id, 'status' => 'accepted'])
               ->getResults();
            $shared_lists = [];
            foreach ($shares as $share) {
               $list = $this->lists
                  ->where(['id' => $share->list_id])
                  ->first(ARRAY_A);
               if ($list) {
                  $owner = get_userdata($list['user_id']);
                  $list['owner_name'] = $owner ? $owner->display_name : 'Unknown';
                  $list['item_count'] = $this->listItems
                     ->where(['list_id' => $list['id']])
                     ->countResults();
                  $list['permission_type'] = $share->permission_type;
                  $list['is_owner'] = false;
                  $list['is_shared'] = true;
                  $shared_lists[] = $list;
               }
            }
            return [
               'success' => true,
               'lists' => [
                  'owned' => $owned,
                  'shared' => $shared_lists
               ]
            ];
         } catch (Exception $e) {
            $this->logError('getAvailableLists', [
               'error' => $e->getMessage(),
               'user_id' => $user_id
            ]);
            return [];
         }
      });
   }
   /**
    * Get favourite counts by type
    */
   public function getFavouriteCounts(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      if (!$this->userCheck($user_id)) {
         return $this->unauthorized();
      }
      $key = "counts_{$user_id}";
      $counts = $this->cache->remember($key, function() use ($user_id) {
         try {
            // Get counts grouped by type using raw query
            $results = $this->favourites->queryResults(
               "SELECT type, COUNT(*) as count FROM {table} WHERE user_id = %d GROUP BY type",
               [$user_id],
               OBJECT_K
            );
            $all_counts = array_fill_keys(
               array_map(fn($type) => str_replace(BASE, '', $type), array_keys($this->valid_types)),
               0
            );
            foreach ($results as $type => $data) {
               $type_key = str_replace(BASE, '', $type);
               $all_counts[$type_key] = (int)$data->count;
            }
            return $all_counts;
         } catch (Exception $e) {
            $this->logError('getFavouriteCounts', [
               'error' => $e->getMessage(),
               'user_id' => $user_id
            ]);
            return array_fill_keys(array_keys($this->valid_types), 0);
         }
      });
      return Response::success(['counts' => $counts]);
   }
   /**
    * Process favourite operations using transactions
    */
   public function processOperation($result, object $operation, array $data)
   {
      $action_map = [
         'favourite_add' => 'addFavourite',
         'favourite_remove' => 'removeFavourite',
         'favourite_batch' => 'batchFavourites',
         'favourite_note' => 'processNote',
         'favourite_list_create' => 'createList',
         'favourite_list_update' => 'updateList',
         'favourite_list_delete' => 'deleteList',
         'favourite_list_add_items' => 'addToList',
         'favourite_list_remove_items' => 'removeFromList',
         'favourite_list_share' => 'shareList',
         'favourite_list_unshare' => 'unshareList',
      ];
      if (!isset($action_map[$operation->type])) {
         return $result;
      }
      try {
         $method = $action_map[$operation->type];
         $response = $this->$method($operation->user_id, $data);
         // Clear cache on success
         if ($response['success'] ?? false) {
            Cache::invalidateItem('favourites', $operation->user_id);
            $this->listsCache->flush();
            $this->sharedListsCache->flush();
         }
         $response_data = [
            'success' => true,
            'items'     => $by_type,
            'has_more'  => false,
         ];
         return $response_data;
         return $response;
      } catch (Exception $e) {
         $this->logError(
            $e->getMessage(),
            [
               'context'   => 'fetchAllFavourites',
               'user'   => $user_id
            ]
         );
         $this->logError('processOperation', [
            'error' => $e->getMessage(),
            'operation_id' => $operation->id,
            'type' => $operation->type
         ]);
         return [
            'success'   => false,
            'favourites'   => [],
            'success' => false,
            'result' => $e->getMessage()
         ];
      }
   }
    /**
     * Handle favourite operations (toggle, notes update)
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response Response with operation result
     */
    public function handleFavouriteOperation(WP_REST_Request $request): WP_REST_Response
    {
        $data = $request->get_json_params();
        $operation = $data['operation'] ?? 'toggle';
        $user_id = get_current_user_id();
   /**
    * Add a favourite using findOrCreate pattern
    */
   protected function addFavourite(int $user_id, array $data): array
   {
      $type = $data['type'];
      $target_id = $data['target_id'];
        $queue = JVB()->queue();
        $operation_id = $data['id'] ?? uniqid('fav_');
      if (!str_starts_with($type, BASE)) {
         $type = BASE . $type;
      }
        error_log('Favourite Request: '.print_r($data, true));
      if (!isset($this->valid_types[$type])) {
         return ['success' => false, 'result' => 'Invalid type'];
      }
        switch ($operation) {
            case 'toggle':
                $adds = $request->get_param('adds') ?? [];
                $removes = $request->get_param('removes') ?? [];
      // Use findOrCreate pattern
      $result = $this->favourites->findOrCreate([
         'user_id' => $user_id,
         'type' => $type,
         'target_id' => $target_id
      ]);
                $queue->queueOperation(
                    'favourites_batch',
                    $request->get_param('user'),
                    [
                        'adds'  => $adds,
                        'removes'   => $removes
                    ],
                    [
                        'count'   => count($adds) + count($removes),
                  'chunk_key'    => ['adds', 'removes'],
                  'chunk_size'   => 20,
                        'priority'          => 'normal',
                        'operation_id'      => $request->get_param('id')
                    ]
                );
      if ($result['created']) {
         $this->updateFavouriteCount($type, $target_id);
         $this->maybeNotifyOwner($type, $target_id, $user_id);
      }
                break;
      return [
         'success' => true,
         'result' => [
            'action' => $result['created'] ? 'added' : 'already_exists',
            'favourite_id' => $result['id'],
            'count' => $this->getFavouriteCount($type, $target_id)
         ]
      ];
   }
            case 'update_notes':
                // Handle notes update
                if (!array_key_exists('target_id', $data)) {
                    return $this->createErrorResponse(
                        self::ERROR_MISSING_PARAMS,
                        'Type and target ID are required',
                        400
                    );
                }
   /**
    * Remove a favourite
    */
   protected function removeFavourite(int $user_id, array $data): array
   {
      $type = $data['type'];
      $target_id = $data['target_id'];
                $ids = explode(',', $data['target_id']);
                foreach ($ids as $key => $id) {
                    $ids[$key] = (int)$id;
                }
                $ids = implode(',', $ids);
      if (!str_starts_with($type, BASE)) {
         $type = BASE . $type;
      }
                $queue->queueOperation(
                    'favourite_notes',
                    $user_id,
                    [
                        'target_id' => $ids,
                        'notes' => sanitize_textarea_field($data['notes'] ?? '')
                    ],
                    [
                        'count' => 1,
                        'operation_id' => $operation_id
                    ]
                );
      $deleted = $this->favourites->where([
         'user_id' => $user_id,
         'type' => $type,
         'target_id' => $target_id
      ])->deleteResults();
                break;
      if ($deleted) {
         $this->updateFavouriteCount($type, $target_id);
         $this->removeRelatedNotifications($type, $target_id, $user_id);
      }
            default:
                return $this->createErrorResponse(
                    self::ERROR_INVALID_OPERATION,
                    'Invalid operation',
                    400
                );
        }
      return [
         'success' => true,
         'result' => [
            'action' => $deleted ? 'removed' : 'not_found',
            'count' => $this->getFavouriteCount($type, $target_id)
         ]
      ];
   }
        return new WP_REST_Response([
            'success' => true,
            'message' => __('Operation queued', 'jvb'),
            'operation_id' => $operation_id,
            'queue_status' => $queue->getQueueStatus()
        ]);
    }
   /**
    * Batch favourites using transaction
    */
   protected function batchFavourites(int $user_id, array $data): array
   {
      $items = $data['items'] ?? [];
    /**
     * Get user's favourite lists
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response Response with lists data
     */
    public function getLists(WP_REST_Request $request):WP_REST_Response
    {
        $user_id = get_current_user_id();
      if (empty($items)) {
         return ['success' => false, 'result' => 'No items provided'];
      }
      if (!$user_id || !$this->userCheck($user_id)) {
         return new WP_REST_Response([
            'success' => false,
            'message' => 'Invalid user'
      return $this->favourites->transaction(function($table) use ($user_id, $items) {
         $results = [
            'added' => 0,
            'removed' => 0,
            'errors' => []
         ];
         foreach ($items as $item) {
            try {
               $type = BASE . ($item['type'] ?? '');
               $target_id = absint($item['target_id'] ?? 0);
               $action = $item['action'] ?? 'add';
               if ($action === 'add') {
                  $result = $table->findOrCreate([
                     'user_id' => $user_id,
                     'type' => $type,
                     'target_id' => $target_id
                  ]);
                  if ($result['created']) $results['added']++;
               } else {
                  $deleted = $table->where([
                     'user_id' => $user_id,
                     'type' => $type,
                     'target_id' => $target_id
                  ])->deleteResults();
                  if ($deleted) $results['removed']++;
               }
               $this->updateFavouriteCount($type, $target_id);
            } catch (Exception $e) {
               $results['errors'][] = $item['target_id'] ?? 'unknown';
            }
         }
         return [
            'success' => true,
            'result' => $results
         ];
      });
   }
   /**
    * Create a list using transaction
    */
   protected function createList(int $user_id, array $data): array
   {
      $name = sanitize_text_field($data['name'] ?? 'Untitled List');
      $description = sanitize_textarea_field($data['description'] ?? '');
      $items = $data['items'] ?? [];
      return $this->lists->transaction(function($table) use ($user_id, $name, $description, $items) {
         $list_id = $table->create([
            'user_id' => $user_id,
            'name' => $name,
            'description' => $description,
         ]);
         if (!$list_id) {
            throw new Exception('Failed to create list');
         }
         $added_count = 0;
         if (!empty($items)) {
            $result = $this->addItemsToList($list_id, $items, $user_id);
            $added_count = $result['added'];
         }
         return [
            'success' => true,
            'result' => [
               'list_id' => $list_id,
               'name' => $name,
               'added_items' => $added_count,
            ]
         ];
      });
   }
   /**
    * Add items to list with bulk insert
    */
   protected function addItemsToList(int $list_id, array $items, int $user_id): array
   {
      if (empty($items)) {
         return ['added' => 0, 'errors' => []];
      }
      $added = 0;
      $errors = [];
      try {
         // Group items by type
         $items_by_type = [];
         foreach ($items as $item) {
            if (empty($item['type']) || !isset($item['target_id'])) {
               $errors[] = ['message' => 'Item missing type or target_id', 'item' => $item];
               continue;
            }
            $type = str_starts_with($item['type'], BASE)
               ? $item['type']
               : BASE . $item['type'];
            if (!isset($items_by_type[$type])) {
               $items_by_type[$type] = [];
            }
            $items_by_type[$type][] = absint($item['target_id']);
         }
         foreach ($items_by_type as $type => $target_ids) {
            // Get existing items to avoid duplicates
            $existing = $this->listItems
               ->where(['list_id' => $list_id, 'item_type' => $type])
               ->getResults();
            $existing_map = [];
            foreach ($existing as $item) {
               $existing_map[$item->item_id] = true;
            }
            // Get favourite IDs in bulk
            $favs = $this->favourites
               ->where(['user_id' => $user_id, 'type' => $type])
               ->getResults();
            $fav_map = [];
            foreach ($favs as $fav) {
               $fav_map[$fav->target_id] = $fav->id;
            }
            // Prepare items for bulk insert
            $to_insert = [];
            foreach ($target_ids as $target_id) {
               if (isset($existing_map[$target_id])) {
                  continue;
               }
               $to_insert[] = [
                  'list_id' => $list_id,
                  'item_type' => $type,
                  'item_id' => $target_id,
                  'favourite_id' => $fav_map[$target_id] ?? null
               ];
            }
            // Bulk insert
            if (!empty($to_insert)) {
               $columns = ['list_id', 'item_type', 'item_id', 'favourite_id'];
               $result = $this->listItems->bulkInsert($to_insert, $columns);
               $added += $result;
            }
         }
         // Update list timestamp
         $this->lists->where(['id' => $list_id])->updateResults([]);
         return ['added' => $added, 'errors' => $errors];
      } catch (Exception $e) {
         $this->logError('addItemsToList', [
            'error' => $e->getMessage(),
            'user_id' => $user_id,
            'list_id' => $list_id,
         ]);
         $errors[] = ['message' => $e->getMessage()];
         return ['added' => 0, 'errors' => $errors];
      }
   }
   /**
    * Clean up favourites when a post is deleted
    */
   public function cleanupPostFavourites(int $post_id): void
   {
      try {
         $type = get_post_type($post_id);
         if (!$type) return;
         $type = BASE . $type;
         // Delete using fluent interface
         $this->favourites->where([
            'type' => $type,
            'target_id' => $post_id
         ])->deleteResults();
         $this->listItems->where([
            'item_type' => $type,
            'item_id' => $post_id
         ])->deleteResults();
      } catch (Exception $e) {
         $this->logError('cleanupPostFavourites', [
            'error' => $e->getMessage(),
            'post_id' => $post_id
         ]);
      }
   }
      // Check HTTP cache headers
      $cache_check = $this->checkUserHeaders($request, $user_id, 'favourites_lists');
      if ($cache_check) {
         return $cache_check;
      }
   /**
    * Clean up favourites when a term is deleted
    */
   public function cleanupTermFavourites(int $term_id, int $tt_id, string $taxonomy): void
   {
      try {
         if (!isset($this->valid_types[$taxonomy])) {
            return;
         }
        $list_id = $request->get_param('id');
         // Delete using fluent interface
         $this->favourites->where([
            'type' => $taxonomy,
            'target_id' => $term_id
         ])->deleteResults();
        if ($list_id) {
            $response = $this->getListDetails($list_id, $user_id);
        } else {
            $response = $this->getAvailableLists($user_id);
        }
         $this->listItems->where([
            'item_type' => $taxonomy,
            'item_id' => $term_id
         ])->deleteResults();
        $response = new WP_REST_Response($response);
      return $this->addCacheHeaders($response);
    }
    /**
     * Get lists available to a user (owned and shared)
     *
     * @param int $user_id User ID
     * @param bool $include_shared Include lists shared with user
     * @return array Lists data
     */
    public function getAvailableLists(int $user_id, bool $include_shared = true):array
    {
        if (!$this->checkUser($user_id)) {
            return [];
        }
        $key = sprintf(
            'user_%d_lists',
            $user_id
        );
        if ($include_shared) {
            $key = $key.'_shared';
        }
        $cache = $this->cache->get($key, 'favourites_lists');
        if ($cache) {
            return $cache;
        }
        global $wpdb;
        error_log('Attempting to get available lists..');
        $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
        $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
        $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
        try {
            // Get owned lists
            $owned_query = $wpdb->prepare(
                "SELECT l.*,
                COUNT(DISTINCT i.id) as item_count,
                TRUE as is_owner,
                FALSE as is_shared
            FROM {$lists_table} l
            LEFT JOIN {$items_table} i ON l.id = i.list_id
            WHERE l.user_id = %d
            GROUP BY l.id
            ORDER BY l.created_at DESC",
                $user_id
            );
            $lists = $wpdb->get_results($owned_query);
            error_log('Lists result: '.print_r($lists, true));
            // Add shared lists if requested
            if ($include_shared) {
                $shared_query = $wpdb->prepare(
                    "SELECT l.*,
                    u.display_name as owner_name,
                    COUNT(DISTINCT i.id) as item_count,
                    s.permission_type,
                    FALSE as is_owner,
                    TRUE as is_shared
                FROM {$lists_table} l
                JOIN {$shares_table} s ON l.id = s.list_id
                JOIN {$wpdb->users} u ON l.user_id = u.ID
                LEFT JOIN {$items_table} i ON l.id = i.list_id
                WHERE s.user_id = %d
                GROUP BY l.id
                ORDER BY l.created_at DESC",
                    $user_id
                );
                $shared_lists = $wpdb->get_results($shared_query);
                error_log('Shared lists: '.print_r($shared_lists, true));
                $lists = [
                    'owned' => $lists,
                    'shared'=> $shared_lists,
                ];
            }
            // Cache result
            $this->cache->set($key, ['success' => true, 'lists'=>$lists], 'favourites_lists');
            error_log('Lists: '.print_r($lists, true));
            return [
                'success' => true,
                'lists' => $lists
            ];
        } catch (Exception $e) {
            JVB()->error()->log(
                'favourites',
                'Error getting available lists: ' . $e->getMessage(),
                ['user_id' => $user_id],
                'error'
            );
            return [];
        }
    }
    /**
     * Get detailed information for a single list
     *
     * @param int $list_id List ID
     * @param int $user_id User ID
     * @return WP_REST_Response|WP_Error Response with list details or error
     */
    protected function getListDetails(int $list_id, int $user_id):WP_REST_Response
    {
        $key = sprintf(
            'user_%d_list_%d',
            $user_id,
            $list_id
        );
        $cache = $this->cache->get($key, 'favourites_lists');
        if ($cache) {
            return new WP_REST_Response($cache);
        }
        //No cache, build it again
        global $wpdb;
        $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
        $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
        $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
        $pending_shares_table = $wpdb->prefix . BASE . 'favourites_pending_shares';
        // Check if user has access to this list
        $list_access = $wpdb->get_row($wpdb->prepare("
            SELECT l.*,
                CASE WHEN l.user_id = %d THEN 1 ELSE 0 END as is_owner,
                CASE WHEN s.id IS NOT NULL THEN 1 ELSE 0 END as is_shared
            FROM {$lists_table} l
            LEFT JOIN {$shares_table} s ON l.id = s.list_id AND s.user_id = %d
            WHERE l.id = %d
        ", $user_id, $user_id, $list_id));
        if (!$list_access || (!$list_access->is_owner && !$list_access->is_shared)) {
            return $this->createErrorResponse(
                self::ERROR_ACCESS_DENIED,
                'You do not have access to this list',
                403
            );
        }
        // Get list items
        $items = $wpdb->get_results($wpdb->prepare("
            SELECT i.*, f.user_id, f.type, f.target_id, f.notes
            FROM {$items_table} i
            LEFT JOIN {$wpdb->prefix}" . BASE . "favourites f
                ON i.favourite_id = f.id
            WHERE i.list_id = %d
            ORDER BY i.added_at DESC
        ", $list_id));
        // Format items using batch processing to reduce queries
        $formatted_items = [];
        $dummy_items = [];
        $favourite_items = [];
        foreach ($items as $item) {
            if ($item->favourite_id === null) {
                // Create a dummy favourite object
                $dummy_items[] = (object)[
                    'type' => $item->item_type,
                    'target_id' => $item->item_id,
                    'date_added' => $item->added_at
                ];
            } else {
                // Use the joined favourite data
                $favourite_items[] = (object)[
                    'id' => $item->favourite_id,
                    'user_id' => $item->user_id,
                    'type' => $item->type,
                    'target_id' => $item->target_id,
                    'notes' => $item->notes,
                    'date_added' => $item->added_at
                ];
            }
        }
        // Process items in batches to reduce DB queries
        $formatted_dummy_items = $this->formatItems($dummy_items);
        $formatted_favourite_items = $this->formatItems($favourite_items);
        // Combine formatted items
        $formatted_items = array_merge($formatted_dummy_items, $formatted_favourite_items);
        // Get shared users if owner
        $shared_users = [];
        if ($list_access->is_owner) {
            // Get active shares
            $active_shares = $wpdb->get_results($wpdb->prepare("
                SELECT s.*, u.user_email as email, u.display_name
                FROM {$shares_table} s
                JOIN {$wpdb->users} u ON s.user_id = u.ID
                WHERE s.list_id = %d
            ", $list_id));
            // Get pending shares
            $pending_shares = $wpdb->get_results($wpdb->prepare("
                SELECT * FROM {$pending_shares_table}
                WHERE list_id = %d
            ", $list_id));
            // Format shared users
            foreach ($active_shares as $share) {
                $shared_users[] = [
                    'email' => $share->email,
                    'name' => $share->display_name,
                    'permission_type' => $share->permission_type,
                    'date_added' => $share->created_at,
                    'status' => 'active'
                ];
            }
            foreach ($pending_shares as $share) {
                $shared_users[] = [
                    'email' => $share->email,
                    'invitation_token' => $share->invitation_token,
                    'date_added' => $share->created_at,
                    'status' => 'pending'
                ];
            }
        }
        // Prepare response data
        $response_data = [
            'success' => true,
            'list' => [
                'id' => (int)$list_access->id,
                'name' => $list_access->name,
                'description' => $list_access->description,
                'created_at' => $list_access->created_at,
                'is_owner' => (bool)$list_access->is_owner,
                'is_shared' => (bool)$list_access->is_shared,
                'items' => $formatted_items,
                'shared_users' => $shared_users
            ]
        ];
        $this->cache->set($key, $response_data, 'favourites_lists');
        return new WP_REST_Response($response_data);
    }
    /**
     * Handle list operations (create, update, delete, add/remove items)
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response Response with operation result
     */
    public function handleListOperation(WP_REST_Request $request):WP_REST_Response
    {
        $data = $request->get_json_params();
        $operation = $data['operation'] ?? '';
        $user_id = get_current_user_id();
        // Get queue system
        $queue = JVB()->queue();
        $operation_id = (array_key_exists('id', $data)) ? $data['id'] : uniqid('list_');
        // Process based on operation type
        switch ($operation) {
            case 'create':
                // Create new list
                if (!array_key_exists('name', $data)) {
                    return $this->createErrorResponse(
                        self::ERROR_MISSING_PARAMS,
                        'List name is required',
                        400
                    );
                }
                $queue->queueOperation(
                    'favourite_list_create',
                    $user_id,
                    [
                        'name' => sanitize_text_field($data['name']),
                        'description' => sanitize_textarea_field($data['description'] ?? ''),
                        'items' => $data['items'] ?? []
                    ],
                    [
                        'count' => 1,
                        'operation_id' => $operation_id
                    ]
                );
                break;
            case 'update':
                // Update list
                if (!array_key_exists('list_id', $data)) {
                    return $this->createErrorResponse(
                        self::ERROR_MISSING_PARAMS,
                        'List ID is required',
                        400
                    );
                }
                $queue->queueOperation(
                    'favourite_list_update',
                    $user_id,
                    [
                        'list_id' => (int)$data['list_id'],
                        'name' => $data['name'] ?? null,
                        'description' => $data['description'] ?? null
                    ],
                    [
                        'count' => 1,
                        'operation_id' => $operation_id
                    ]
                );
                break;
            case 'delete':
                // Delete list
                if (!array_key_exists('list_id', $data)) {
                    return $this->createErrorResponse(
                        self::ERROR_MISSING_PARAMS,
                        'List ID is required',
                        400
                    );
                }
                $queue->queueOperation(
                    'favourite_list_delete',
                    $user_id,
                    [
                        'list_id' => (int)$data['list_id']
                    ],
                    [
                        'count' => 1,
                        'operation_id' => $operation_id
                    ]
                );
                break;
            case 'add_items':
                // Add items to list
                if (!array_key_exists('list_id', $data) || !array_key_exists('items', $data)) {
                    return $this->createErrorResponse(
                        self::ERROR_MISSING_PARAMS,
                        'List ID and items are required',
                        400
                    );
                }
                $queue->queueOperation(
                    'favourite_list_add',
                    $user_id,
                    [
                        'list_id' => (int)$data['list_id'],
                        'items' => $data['items']
                    ],
                    [
                        'count' => 1,
                  'chunk_key' => 'items',
                  'chunk_size' => 20,
                        'operation_id' => $operation_id
                    ]
                );
                break;
            case 'remove_items':
                // Remove items from list
                if (!array_key_exists('list_id', $data) || !array_key_exists('items', $data)) {
                    return $this->createErrorResponse(
                        self::ERROR_MISSING_PARAMS,
                        'List ID and items are required',
                        400
                    );
                }
                $queue->queueOperation(
                    'favourite_list_remove',
                    $user_id,
                    [
                        'list_id' => (int)$data['list_id'],
                        'items' => $data['items']
                    ],
                    [
                        'count' => 1,
                  'chunk_key' => 'items',
                  'chunk_size' => 20,
                        'operation_id' => $operation_id
                    ]
                );
                break;
            default:
                return $this->createErrorResponse(
                    self::ERROR_INVALID_OPERATION,
                    'Invalid list operation',
                    400
                );
        }
        return new WP_REST_Response([
            'success' => true,
            'message' => __('List operation queued', 'jvb'),
            'operation_id' => $operation_id,
            'queue_status' => $queue->getQueueStatus()
        ]);
    }
    /**
     * Get shares for a list
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response Response with shares data
     */
    public function getShares(WP_REST_Request $request):WP_REST_Response
    {
      $user_id = $request->get_param('user');
      if (!$user_id || !$this->userCheck($user_id)) {
         return new WP_REST_Response([
            'success' => false,
            'message' => 'Invalid user'
      } catch (Exception $e) {
         $this->logError('cleanupTermFavourites', [
            'error' => $e->getMessage(),
            'term_id' => $term_id,
            'taxonomy' => $taxonomy
         ]);
      }
   }
      // Check HTTP cache headers
      $cache_check = $this->checkUserHeaders($request, $user_id, 'favourites_shares');
      if ($cache_check) {
         return $cache_check;
   /**
    * Cleanup orphaned favourites using CustomTable query method
    */
   public function cleanupOrphanedFavourites(): bool
   {
      try {
         // Delete favourites for non-existent users
         $this->favourites->query(
            "DELETE f FROM {table} f
                 LEFT JOIN {$GLOBALS['wpdb']->users} u ON f.user_id = u.ID
                 WHERE u.ID IS NULL"
         );
         // Delete favourites for non-existent posts
         $post_types = array_filter(
            array_keys($this->valid_types),
            fn($type) => $this->valid_types[$type]['table'] === 'post'
         );
         foreach ($post_types as $type) {
            $this->favourites->query(
               "DELETE f FROM {table} f
                     LEFT JOIN {$GLOBALS['wpdb']->posts} p ON f.target_id = p.ID
                     WHERE f.type = %s AND p.ID IS NULL",
               [$type]
            );
         }
         return true;
      } catch (Exception $e) {
         $this->logError('cleanupOrphanedFavourites', [
            'error' => $e->getMessage()
         ]);
         return false;
      }
      $list_id = $request->get_param('list_id');
   }
        if (!$list_id) {
            return $this->createErrorResponse(
                self::ERROR_MISSING_PARAMS,
                'List ID is required',
                400
            );
        }
   /**
    * Helper methods
    */
   protected function buildParams(WP_REST_Request $request): array
   {
      $data = $request->get_params();
      $args = ['user' => absint($data['user'])];
        $key = sprintf(
            'user_%d_shares_for_list_%d',
            $user_id,
            $list_id
        );
        $cache = $this->cache->get($key, 'favourites_list_shares');
        if ($cache) {
            return new WP_REST_Response($cache);
        }
      if (!array_key_exists('page', $data)) {
         return $args;
      }
        try {
            global $wpdb;
            $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
            $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
      $args = array_merge($args, [
         'page' => max(1, absint($data['page'] ?? 1)),
         'content' => Registrar::getInstance($data['content']) ? $data['content'] : 'all',
      ]);
            // Verify ownership
            $is_owner = $wpdb->get_var($wpdb->prepare(
                "SELECT 1 FROM {$lists_table} WHERE id = %d AND user_id = %d",
                $list_id,
                $user_id
            ));
      return $this->applyOrderFilters($args, $data);
   }
            if (!$is_owner) {
                return $this->createErrorResponse(
                    self::ERROR_ACCESS_DENIED,
                    'You do not own this list',
                    403
                );
            }
            // Get all shares (both active and pending) in one query
            $query = $wpdb->prepare(
                "SELECT s.*,
             CASE
                WHEN s.user_id IS NOT NULL THEN u.display_name
                ELSE NULL
             END as display_name,
             CASE
                WHEN s.user_id IS NOT NULL THEN u.user_email
                ELSE s.email
             END as email
             FROM {$shares_table} s
             LEFT JOIN {$wpdb->users} u ON s.user_id = u.ID
             WHERE s.list_id = %d
             ORDER BY s.status, s.created_at DESC",
                $list_id
            );
   /**
    * Batch format a collection of favourite items
    *
    * @param array $items Collection of favourite items
    * @return array Formatted items
    */
   protected function formatItems(array $items):array
   {
      if (empty($items)) {
         return [];
      }
            $all_shares = $wpdb->get_results($query);
      // Group items by type to reduce queries
      $items_by_type = [];
      foreach ($items as $item) {
         if (!isset($item->type) || !isset($item->target_id)) {
            continue;
         }
            // Format shares for response
            $shares = [];
            foreach ($all_shares as $share) {
                $formatted_share = [
                    'id' => $share->id,
                    'email' => $share->email,
                    'status' => $share->status,
                    'date_added' => $share->created_at,
                ];
         // Verify item type is valid
         if (!isset($this->valid_types[$item->type])) {
            continue;
         }
                // Add attributes specific to the status
                if ($share->status === 'accepted') {
                    $formatted_share['name'] = $share->display_name;
                    $formatted_share['user_id'] = $share->user_id;
                    $formatted_share['permission_type'] = $share->permission_type;
                } else if ($share->status === 'pending') {
                    // Include invitation token if needed for managing invitations
                    $formatted_share['invitation_token'] = $share->invitation_token;
                }
         $type = $item->type;
         if (!isset($items_by_type[$type])) {
            $items_by_type[$type] = [];
         }
         $items_by_type[$type][] = $item;
      }
                $shares[] = $formatted_share;
            }
      $formatted = [];
            $response_data = [
                'success' => true,
                'list_id' => $list_id,
                'shares' => $shares
            ];
      // Process post-type items in batches
      foreach ($items_by_type as $type => $type_items) {
         $config = $this->valid_types[$type];
            // Cache the results
            $this->cache->set($key, $response_data, 'favourites_list_shares');
         if ($config['table'] === 'post') {
            $formatted = array_merge($formatted, $this->formatPostFavourites($type_items));
         } else {
            $formatted = array_merge($formatted, $this->formatTermFavourites($type_items));
         }
      }
         $response = new WP_REST_Response($response_data);
         return $this->addCacheHeaders($response);
      return $formatted;
   }
   protected function getFavouriteCount(string $type, int $target_id): int
   {
      return $this->favourites->where([
         'type' => $type,
         'target_id' => $target_id
      ])->countResults();
   }
        } catch (Exception $e) {
            return $this->createErrorResponse(
                self::ERROR_PROCESSING,
                'Error retrieving shares: ' . $e->getMessage(),
                500,
                ['user_id' => $user_id, 'list_id' => $list_id]
            );
        }
    }
   protected function updateFavouriteCount(string $type, int $target_id): void
   {
      $count = $this->getFavouriteCount($type, $target_id);
    /**
     * Handle share operations (add/remove)
     *
     * @param WP_REST_Request $request Request object
     * @return WP_REST_Response Response with operation result
     */
    public function handleShare(WP_REST_Request $request):WP_REST_Response
    {
        $data = $request->get_json_params();
        $operation = $data['operation'] ?? '';
        $user_id = get_current_user_id();
      if (str_contains($type, 'post') || in_array($type, array_keys($this->valid_types))) {
         update_post_meta($target_id, BASE.'favourite_count', $count);
      } else {
         update_term_meta($target_id, BASE.'favourite_count', $count);
      }
   }
        $queue = JVB()->queue();
        $operation_id = $data['id'] ?? uniqid('share_');
   /**
    * Notify content owner of new favourite if configured
    *
    * @param string $type Content type
    * @param int $target_id Content ID
    * @param int $user_id User who favourited
    * @return void
    */
   protected function maybeNotifyOwner(string $type, int $target_id, int $user_id): void
   {
      try {
         $owner_id = $this->getContentOwner($type, $target_id);
        if (empty($data['list_id']) || empty($data['email'])) {
            return $this->createErrorResponse(
                self::ERROR_MISSING_PARAMS,
                'List ID and email are required',
                400
            );
        }
         if ($owner_id && $owner_id !== $user_id) {
            JVB()->notification()->addNotification(
               $owner_id,
               'new_favourite',
               [
                  'user_id' => $user_id,
                  'type' => $type,
                  'target_id' => $target_id
               ]
            );
         }
      } catch (Exception $e) {
         // Silent fail - notifications are non-critical
      }
   }
        switch ($operation) {
            case 'add':
                // Share list with email
                if (!is_email($data['email'])) {
                    return $this->createErrorResponse(
                        self::ERROR_MISSING_PARAMS,
                        'Invalid email address',
                        400
                    );
                }
   /**
    * Remove any existing notifications about a favorite action
    *
    * @param int $user_id User who removed the favorite
    * @param string $type Content type
    * @param int $target_id Content ID
    * @return void
    */
   protected function removeRelatedNotifications(int $user_id, string $type, int $target_id):void
   {
      try {
         // Get the content owner(s)
         $owner_ids = $this->getContentOwner($type, $target_id);
         if (!$owner_ids) {
            return;
         }
                $queue->queueOperation(
                    'favourite_list_share',
                    $user_id,
                    [
                        'list_id' => (int)$data['list_id'],
                        'email' => sanitize_email($data['email']),
                        'permission_type' => in_array($data['permission_type'] ?? '', ['view', 'edit'])
                            ? $data['permission_type']
                            : 'view'
                    ],
                    [
                        'count' => 1,
                        'operation_id' => $operation_id
                    ]
                );
                break;
         $owner_ids = (is_array($owner_ids)) ? $owner_ids : [$owner_ids];
            case 'remove':
                // Remove share
                $queue->queueOperation(
                    'favourite_list_unshare',
                    $user_id,
                    [
                        'list_id' => (int)$data['list_id'],
                        'email' => sanitize_email($data['email'])
                    ],
                    [
                        'count' => 1,
                        'operation_id' => $operation_id
                    ]
                );
                break;
         foreach ($owner_ids as $owner_id) {
            // Skip if owner is the same as the user who unfavorited
            if ($owner_id === $user_id) {
               continue;
            }
            case 'accept':
                $data = $request->get_json_params();
                $token = $data['token'] ?? '';
                $email = sanitize_email($data['email'] ?? '');
                $user_id = get_current_user_id(); // Get current user if logged in
            global $wpdb;
            $notifications_table = $wpdb->prefix . BASE . 'notifications';
                $result = $this->acceptListInvitation($token, $email, $user_id > 0 ? $user_id : null);
                break;
            default:
                return $this->createErrorResponse(
                    self::ERROR_INVALID_OPERATION,
                    'Invalid share operation',
                    400
                );
        }
        return new WP_REST_Response([
            'success' => true,
            'message' => __('Share operation queued', 'jvb'),
            'operation_id' => $operation_id,
            'queue_status' => $queue->getQueueStatus()
        ]);
    }
    /**
     * Batch format a collection of favourite items
     *
     * @param array $items Collection of favourite items
     * @return array Formatted items
     */
    protected function formatItems(array $items):array
    {
        if (empty($items)) {
            return [];
        }
        // Group items by type to reduce queries
        $items_by_type = [];
        foreach ($items as $item) {
            if (!isset($item->type) || !isset($item->target_id)) {
                continue;
            }
            // Verify item type is valid
            if (!isset($this->valid_types[$item->type])) {
                continue;
            }
            $type = $item->type;
            if (!isset($items_by_type[$type])) {
                $items_by_type[$type] = [];
            }
            $items_by_type[$type][] = $item;
        }
        $formatted = [];
        // Process post-type items in batches
        foreach ($items_by_type as $type => $type_items) {
            $config = $this->valid_types[$type];
            if ($config['table'] === 'post') {
                $formatted = array_merge($formatted, $this->formatPostFavourites($type_items));
            } else {
                $formatted = array_merge($formatted, $this->formatTermFavourites($type_items));
            }
        }
        return $formatted;
    }
    /**
     * Batch format post-type favourites to reduce queries
     *
     * @param array $items Collection of favourite items of post type
     * @return array Formatted items
     */
    protected function formatPostFavourites(array $items):array
    {
        if (empty($items)) {
            return [];
        }
        $formatted = [];
        $post_ids = array_map(function ($item) {
            return (int)$item->target_id;
        }, $items);
        // Get all posts in one query
        $posts = get_posts([
            'post__in' => $post_ids,
            'post_type' => 'any',
            'posts_per_page' => -1,
            'post_status' => 'any',
        ]);
        // Create a lookup map
        $posts_by_id = [];
        foreach ($posts as $post) {
            $posts_by_id[$post->ID] = $post;
        }
        // Get all thumbnails for artists in one query if needed
        $artist_ids = [];
        foreach ($items as $item) {
            if ($item->type === BASE.'artist') {
                $artist_ids[] = (int)$item->target_id;
            }
        }
        $artist_images = [];
        if (!empty($artist_ids)) {
            global $wpdb;
            $meta_query = $wpdb->prepare(
                "SELECT post_id, meta_value FROM {$wpdb->postmeta}
                WHERE meta_key = %s AND post_id IN (" . implode(',', array_fill(0, count($artist_ids), '%d')) . ")",
                array_merge([BASE.'image'], $artist_ids)
            );
            $results = $wpdb->get_results($meta_query);
            foreach ($results as $result) {
                $artist_images[$result->post_id] = $result->meta_value;
            }
        }
        // Get all thumbnails in one query
        $thumbnail_ids = [];
        foreach ($items as $item) {
            if ($item->type !== BASE.'artist' && isset($posts_by_id[$item->target_id])) {
                $thumb_id = get_post_thumbnail_id($item->target_id);
                if ($thumb_id) {
                    $thumbnail_ids[$item->target_id] = $thumb_id;
                }
            }
        }
        // Format each item
        foreach ($items as $item) {
            $post_id = (int)$item->target_id;
            // Skip if post doesn't exist
            if (!isset($posts_by_id[$post_id])) {
                continue;
            }
            $post = $posts_by_id[$post_id];
            $formatted_item = [
                'id' => $item->id ?? null,
                'type' => str_replace(BASE, '', $item->type),
                'target_id' => $post_id,
                'date_added' => $item->date_added ?? current_time('mysql'),
                'notes' => $item->notes ?? '',
                'url' => get_permalink($post),
                'title' => $post->post_title,
                'author' => [
                    'id' => $post->post_author,
                    'name' => get_the_author_meta('display_name', $post->post_author)
                ]
            ];
            // Add thumbnail
            if ($item->type === BASE.'artist') {
                $meta_value = $artist_images[$post_id] ?? null;
                $formatted_item['thumbnail'] = $meta_value ? jvbFormatImage($meta_value, 'medium', 'medium') : null;
            } else {
                $thumb_id = $thumbnail_ids[$post_id] ?? null;
                $formatted_item['thumbnail'] = $thumb_id ? jvbFormatImage($thumb_id, 'medium', 'medium') : null;
            }
            $formatted[] = $formatted_item;
        }
        return $formatted;
    }
    /**
     * Batch format term-type favourites to reduce queries
     *
     * @param array $items Collection of favourite items of term type
     * @return array Formatted items
     */
    protected function formatTermFavourites(array $items):array
    {
        if (empty($items)) {
            return [];
        }
        $formatted = [];
        // Group by taxonomy
        $terms_by_taxonomy = [];
        foreach ($items as $item) {
            $tax = $item->type;
            if (!isset($terms_by_taxonomy[$tax])) {
                $terms_by_taxonomy[$tax] = [];
            }
            $terms_by_taxonomy[$tax][] = (int)$item->target_id;
        }
        // Get all terms by taxonomy
        $terms_by_id = [];
        foreach ($terms_by_taxonomy as $taxonomy => $term_ids) {
            $terms = get_terms([
                'taxonomy' => $taxonomy,
                'include' => $term_ids,
                'hide_empty' => false,
            ]);
            if (!is_wp_error($terms)) {
                foreach ($terms as $term) {
                    $terms_by_id[$taxonomy . '_' . $term->term_id] = $term;
                }
            }
        }
        // Get all shop images in one query if needed
        $shop_ids = [];
        foreach ($items as $item) {
            if ($item->type === BASE.'shop') {
                $shop_ids[] = (int)$item->target_id;
            }
        }
        $shop_images = [];
        if (!empty($shop_ids)) {
            global $wpdb;
            $meta_query = $wpdb->prepare(
                "SELECT term_id, meta_value FROM {$wpdb->termmeta}
                WHERE meta_key = %s AND term_id IN (" . implode(',', array_fill(0, count($shop_ids), '%d')) . ")",
                array_merge([BASE.'image'], $shop_ids)
            );
            $results = $wpdb->get_results($meta_query);
            foreach ($results as $result) {
                $shop_images[$result->term_id] = $result->meta_value;
            }
        }
        // Format each item
        foreach ($items as $item) {
            $term_id = (int)$item->target_id;
            $key = $item->type . '_' . $term_id;
            // Skip if term doesn't exist
            if (!isset($terms_by_id[$key])) {
                continue;
            }
            $term = $terms_by_id[$key];
            $formatted_item = [
                'id' => $item->id ?? null,
                'type' => str_replace(BASE, '', $item->type),
                'target_id' => $term_id,
                'date_added' => $item->date_added ?? current_time('mysql'),
                'notes' => $item->notes ?? '',
                'title' => $term->name,
                'url' => get_term_link($term)
            ];
            // Add thumbnail for shops
            if ($item->type === BASE.'shop') {
                $meta_value = $shop_images[$term_id] ?? null;
                $formatted_item['thumbnail'] = $meta_value ? jvbFormatImage($meta_value, 'medium', 'medium') : null;
            }
            $formatted[] = $formatted_item;
        }
        return $formatted;
    }
    /**
     * Get counts of favourites by type for a user
     *
     * @param int $user_id User ID
     * @return array Counts by type
     */
    public function getFavouriteCounts(int $user_id, bool $show_all = true):array
    {
        $key = 'favourite_counts_by_type_' . $user_id;
        $key .= ($show_all) ? '_all' : '_not_all';
        $cache = $this->cache->get($key);
        if ($cache) {
            return $cache;
        }
        try {
            global $wpdb;
            $table = $wpdb->prefix . BASE.'favourites';
            $counts = $wpdb->get_results($wpdb->prepare("
            SELECT type, COUNT(*) as count
            FROM {$table}
            WHERE user_id = %d
            GROUP BY type
        ", $user_id), OBJECT_K);
            $all_counts = [];
            if ($show_all) {
                // Fill in zeros for types with no favourites
                $all_counts = array_fill_keys(array_keys($this->valid_types), 0);
                $temp = [];
                foreach ($all_counts as $type => $count) {
                    $type_key = str_replace(BASE, '', $type);
                    $temp[$type_key] = $count;
                }
                $all_counts = $temp;
            }
            foreach ($counts as $type => $data) {
                $type_key = str_replace(BASE, '', $type);
                $all_counts[$type_key] = (int)$data->count;
            }
            $this->cache->set($key, $all_counts);
            return $all_counts;
        } catch (Exception $e) {
            JVB()->error()->log(
                'favourites',
                'Error getting counts by type: ' . $e->getMessage(),
                ['user_id' => $user_id],
                'warning'
            );
            return array_fill_keys(array_keys($this->valid_types), 0);
        }
    }
    /**
     * Process batch favourites operation
     *
     * @param int $user_id User ID
     * @param array $data Operation data
     * @return array Operation result
     */
    protected function processBatches(int $user_id, array $data):array
    {
        error_log('Processing Batch Operation');
        if (empty($data['adds']) && empty($data['removes'])) {
            return [
                'success'   => true,
                'result'   => array()
            ];
        }
        error_log('Proceeding to TRANSACTION');
        global $wpdb;
        // Start transaction
        $wpdb->query('START TRANSACTION');
        try {
            // Collect notifications to send after transaction
            $notifications = [];
            // Process adds
            foreach ($data['adds'] as $item) {
                $result = $this->addFavourite($user_id, $item['type'], $item['target_id']);
                if (is_wp_error($result)) {
                    $results[] = [
                        'success' => false,
                        'error' => $result->get_error_message(),
                        'type' => $item['type'],
                        'target_id' => $item['target_id']
                    ];
                } else {
                    $results[] = array_merge($item, $result);
                    // If notification needed, add to collection instead of sending immediately
                    if (isset($this->valid_types[$item['type']]) && $this->valid_types[$item['type']]['notify_owner']) {
                        $owner_ids = $this->getContentOwner($item['type'], $item['target_id']);
                        if ($owner_ids) {
                            $owner_ids = (is_array($owner_ids)) ? $owner_ids : [$owner_ids];
                            $type_label = str_replace(BASE, '', $item['type']);
                            foreach ($owner_ids as $owner_id) {
                                if ($owner_id !== $user_id) {
                                    $notifications[] = [
                                        'owner_id' => $owner_id,
                                        'type' => 'new_favourite',
                                        'action_user_id' => $user_id,
                                        'message' => sprintf(
                                            '%s favorited your %s',
                                            jvbShareName($user_id),
                                            $type_label
                                        ),
                                        'target_id' => $item['target_id'],
                                        'target_type' => $item['type'],
                                        'context' => [
                                            'favourite_user_id' => $user_id,
                                            'content_type' => $item['type'],
                                            'content_id' => $item['target_id'],
                                        ]
                                    ];
                                }
                            }
                        }
                    }
                }
            }
            // Process removes
            foreach ($data['removes'] as $item) {
                $result = $this->removeFavourite($user_id, $item['type'], $item['target_id']);
                if (is_wp_error($result)) {
                    $results[] = [
                        'success' => false,
                        'error' => $result->get_error_message(),
                        'type' => $item['type'],
                        'target_id' => $item['target_id']
                    ];
                } else {
                    $results[] = array_merge($item, $result);
                }
            }
            $wpdb->query('COMMIT');
            // Send all notifications at once after successful commit
            $manager = JVB()->notification();
            if (!empty($notifications)) {
                foreach ($notifications as $notification) {
                    $manager->addNotification(
                        $notification['owner_id'],
                        $notification['type'],
                        $notification['action_user_id'],
                        $notification['message'],
                        $notification['target_id'],
                        $notification['target_type'],
                        $notification['context']
                    );
                }
            }
            error_log('Results: '.print_r($results, true));
            $this->cache->invalidate('favourite_counts_by_type_' . $user_id.'_all');
            $this->cache->invalidate('favourite_counts_by_type_' . $user_id.'_not_all');
            return [
                'success' => true,
                'result' => $results
            ];
        } catch (Exception $e) {
            // Something went wrong, roll back changes
            $wpdb->query('ROLLBACK');
            JVB()->error()->log(
                'favourites',
                'Batch operation failed: ' . $e->getMessage(),
                ['user_id' => $user_id, 'item_count' => count($items ?? [])],
                'error'
            );
            error_log('Error: '.print_r($e->getMessage(), true));
            return [
                'success'   => false,
                'result'   =>  $e->getMessage()
            ];
        }
    }
    /**
     * Add a favourite
     *
     * @param int $user_id User ID
     * @param string $type Content type
     * @param int $target_id Target content ID
     * @return array Operation result
     */
    protected function addFavourite(int $user_id, string $type, int $target_id):array
    {
        if (!str_starts_with($type, BASE)) {
            $type = BASE.$type;
        }
        // Validate type
        if (!isset($this->valid_types[$type])) {
            return [
                'success' => false,
                'message' => 'Invalid type',
                'type' => $type,
                'target_id' => $target_id
            ];
        }
        try {
            global $wpdb;
            $table = $wpdb->prefix . BASE . 'favourites';
            // Check if already favourited
            $exists = $wpdb->get_var($wpdb->prepare(
                "SELECT 1 FROM {$table}
             WHERE user_id = %d AND type = %s AND target_id = %d
             LIMIT 1",
                $user_id,
                $type,
                $target_id
            ));
            if ($exists) {
                $result = [
                    'success' => true,
                    'action' => 'already_exists',
                    'type' => str_replace(BASE, '', $type),
                    'target_id' => $target_id,
                    'count' => $this->getFavouriteCount($type, $target_id)
                ];
                // Fire after action even for already existing
                do_action('jvb_after_favourite_add', $user_id, $type, $target_id, $result);
                return $result;
            }
            // Insert new favourite
            $inserted = $wpdb->insert(
                $table,
                [
                    'user_id' => $user_id,
                    'type' => $type,
                    'target_id' => $target_id,
                    'date_added' => current_time('mysql')
                ],
                ['%d', '%s', '%d', '%s']
            );
            if ($inserted === false) {
                throw new Exception($wpdb->last_error);
            }
            // Get new favourite ID
            $favourite_id = $wpdb->insert_id;
            // Update favourite count
            $this->updateFavouriteCount($type, $target_id);
            // Notify owner if needed
            $this->maybeNotifyOwner($type, $target_id, $user_id);
            return [
                'success' => true,
                'action' => 'added',
                'favourite_id' => $favourite_id,
                'type' => str_replace(BASE, '', $type),
                'target_id' => $target_id,
                'count' => $this->getFavouriteCount($type, $target_id)
            ];
        } catch (Exception $e) {
            JVB()->error()->log(
                'favourites',
                'Error adding favourite: ' . $e->getMessage(),
                ['user_id' => $user_id, 'type' => $type, 'target_id' => $target_id],
                'error'
            );
            $result = [
                'success' => false,
                'message' => $e->getMessage(),
                'type' => str_replace(BASE, '', $type),
                'target_id' => $target_id
            ];
            // Fire error action
            do_action('jvb_favourite_add_error', $user_id, $type, $target_id, $e->getMessage());
            return $result;
        }
    }
    protected function removeFavourite(int $user_id, string $type, int $target_id)
    {
        try {
            if (!str_starts_with($type, BASE)) {
                $type = BASE.$type;
            }
            // Validate type
            if (!isset($this->valid_types[$type])) {
                return [
                    'success' => false,
                    'message' => 'Invalid type',
                    'type' => $type,
                    'target_id' => $target_id
                ];
            }
            global $wpdb;
            $table = $wpdb->prefix . BASE . 'favourites';
            // Check if favorite exists before deleting
            $exists = $wpdb->get_var($wpdb->prepare(
                "SELECT 1 FROM {$table}
             WHERE user_id = %d AND type = %s AND target_id = %d
             LIMIT 1",
                $user_id,
                $type,
                $target_id
            ));
            if (!$exists) {
                return [
                    'success' => true,
                    'action' => 'already_removed',
                    'type' => str_replace(BASE, '', $type),
                    'target_id' => $target_id,
                    'count' => $this->getFavouriteCount($type, $target_id)
                ];
            }
            $deleted = $wpdb->delete(
                $table,
                [
                    'user_id' => $user_id,
                    'type' => $type,
                    'target_id' => $target_id
                ],
                ['%d', '%s', '%d']
            );
            if ($deleted === false) {
                throw new Exception($wpdb->last_error);
            }
            // Update favourite count
            $this->updateFavouriteCount($type, $target_id);
            // Remove any related notifications
            $this->removeRelatedNotifications($user_id, $type, $target_id);
            // Invalidate cache
            CacheManager::invalidateGroup($this->cache_name);
            CacheManager::invalidateGroup('favourites_lists');
            return [
                'success' => true,
                'action' => 'removed',
                'type' => str_replace(BASE, '', $type),
                'target_id' => $target_id,
                'count' => $this->getFavouriteCount($type, $target_id)
            ];
        } catch (Exception $e) {
            JVB()->error()->log(
                'favourites',
                'Error removing favourite: ' . $e->getMessage(),
                ['user_id' => $user_id, 'type' => $type, 'target_id' => $target_id],
                'error'
            );
            return [
                'success' => false,
                'message' => $e->getMessage(),
                'type' => str_replace(BASE, '', $type),
                'target_id' => $target_id
            ];
        }
    }
    /**
     * Remove any existing notifications about a favorite action
     *
     * @param int $user_id User who removed the favorite
     * @param string $type Content type
     * @param int $target_id Content ID
     * @return void
     */
    protected function removeRelatedNotifications(int $user_id, string $type, int $target_id):void
    {
        try {
            // Get the content owner(s)
            $owner_ids = $this->getContentOwner($type, $target_id);
            if (!$owner_ids) {
                return;
            }
            $owner_ids = (is_array($owner_ids)) ? $owner_ids : [$owner_ids];
            foreach ($owner_ids as $owner_id) {
                // Skip if owner is the same as the user who unfavorited
                if ($owner_id === $user_id) {
                    continue;
                }
                global $wpdb;
                $notifications_table = $wpdb->prefix . BASE . 'notifications';
                // Find recent (within last 30 days) new_favourite notifications from this user for this content
                $notifications = $wpdb->get_results($wpdb->prepare(
                    "SELECT id FROM {$notifications_table}
            // Find recent (within last 30 days) new_favourite notifications from this user for this content
            $notifications = $wpdb->get_results($wpdb->prepare(
               "SELECT id FROM {$notifications_table}
                WHERE owner_id = %d
                AND action_user_id = %d
                AND type = 'new_favourite'
                AND target_id = %d
                AND target_type = %s
                AND created_at > DATE_SUB(%s, INTERVAL 30 DAY)",
                    $owner_id,
                    $user_id,
                    $target_id,
                    $type,
                    current_time('mysql')
                ));
               $owner_id,
               $user_id,
               $target_id,
               $type,
               current_time('mysql')
            ));
                if (empty($notifications)) {
                    continue;
                }
            if (empty($notifications)) {
               continue;
            }
                // Delete the notifications
                foreach ($notifications as $notification) {
                    $wpdb->delete(
                        $notifications_table,
                        ['id' => $notification->id],
                        ['%d']
                    );
                }
            // Delete the notifications
            foreach ($notifications as $notification) {
               $wpdb->delete(
                  $notifications_table,
                  ['id' => $notification->id],
                  ['%d']
               );
            }
                // Invalidate notification cache for this user
                if (method_exists(JVB()->notification(), 'clearNotificationCache')) {
                    JVB()->notification()->clearNotificationCache($owner_id);
                }
            }
        } catch (Exception $e) {
            // Log but continue
            JVB()->error()->log(
                'favourites',
                'Error removing related notifications: ' . $e->getMessage(),
                ['type' => $type, 'target_id' => $target_id, 'user_id' => $user_id],
                'warning'
            );
        }
    }
            // Invalidate notification cache for this user
//                if (method_exists(JVB()->notification(), 'clearNotificationCache')) {
//                    JVB()->notification()->clearNotificationCache($owner_id);
//                }
         }
      } catch (Exception $e) {
         // Log but continue
         JVB()->error()->log(
            'favourites',
            'Error removing related notifications: ' . $e->getMessage(),
            ['type' => $type, 'target_id' => $target_id, 'user_id' => $user_id],
            'warning'
         );
      }
   }
    /**
     * Process favourite notes update with improved transaction handling
     *
     * @param int $user_id User ID
     * @param array $data Operation data
     * @return array Operation result
     */
    protected function processNote(int $user_id, array $data):array
    {
        global $wpdb;
        $result = [];
   public function maybeAcceptListInvite(int $user_id, string $email, array $data):void
   {
      if (array_key_exists('list_token', $data) && !empty($data['list_token'])) {
         $this->acceptListInvitation($data['list_token'], $email);
      }
   }
        // Start transaction
        $wpdb->query('START TRANSACTION');
   /**
    * Get detailed information for a single list
    */
   protected function getListDetails(int $list_id, int $user_id): array
   {
      $key = "list_{$list_id}_user_{$user_id}";
        try {
            $IDs = explode(',', $data['target_id']);
            $results = [];
            foreach ($IDs as $ID) {
                $target_id = absint($ID);
                if ($target_id <= 0) {
                    throw new Exception('Invalid target ID');
                }
                $notes = sanitize_textarea_field($data['notes'] ?? '');
      return $this->listsCache->remember($key, function () use ($list_id, $user_id) {
         try {
            // Check access - either owner or has share
            $is_owner = $this->lists->where([
               'id' => $list_id,
               'user_id' => $user_id
            ])->existsInQuery();
                $table = $wpdb->prefix . BASE . 'favourites';
            $share = null;
            if (!$is_owner) {
               $share = $this->listShares->where([
                  'list_id' => $list_id,
                  'user_id' => $user_id,
                  'status' => 'accepted'
               ])->first();
            }
                // Check if favourite exists
                $favourite_id = $wpdb->get_var($wpdb->prepare(
                    "SELECT id FROM {$table}
             WHERE user_id = %d AND target_id = %d",
                    $user_id, $target_id
                ));
            if (!$is_owner && !$share) {
               return [
                  'success' => false,
                  'message' => 'You do not have access to this list'
               ];
            }
                if (!$favourite_id) {
                    $result[] = [
                        'success' => false,
                        'target_id' => $target_id,
                        'message' => __('No favourite id found...', 'jvb')
                    ];
                } else {
                    // Update existing favourite
                    $updated = $wpdb->update(
                        $table,
                        ['notes' => $notes],
                        ['id' => $favourite_id],
                        ['%s'],
                        ['%d']
                    );
            // Get list details
            $list = $this->lists->where(['id' => $list_id])->first(ARRAY_A);
            if (!$list) {
               return [
                  'success' => false,
                  'message' => 'List not found'
               ];
            }
                    if ($updated === false) {
                        throw new Exception($wpdb->last_error);
                    }
            // Get list items
            $items = $this->listItems
               ->where(['list_id' => $list_id])
               ->orderBy('added_at', 'DESC')
               ->getResults();
                    $result[] = [
                        'success' => true,
                        'action' => 'updated_notes',
                        'favourite_id' => $favourite_id,
                        'target_id' => $target_id
                    ];
                }
            }
            // Format items - convert to favourite-like objects
            $formatted_items = [];
            foreach ($items as $item) {
               // Try to get the actual favourite record if it exists
               if ($item->favourite_id) {
                  $fav = $this->favourites->where(['id' => $item->favourite_id])->first();
                  if ($fav) {
                     $formatted_items[] = $fav;
                     continue;
                  }
               }
            // If we got here, everything worked - commit the transaction
            $wpdb->query('COMMIT');
               // Create dummy favourite object for formatting
               $formatted_items[] = (object)[
                  'type' => $item->item_type,
                  'target_id' => $item->item_id,
                  'date_added' => $item->added_at
               ];
            }
            return [
            'success'   => true,
            'results'   => $result
            // Get shared users if owner
            $shared_users = [];
            if ($is_owner) {
               $shares = $this->listShares
                  ->where(['list_id' => $list_id])
                  ->orderBy('created_at', 'DESC')
                  ->getResults();
               foreach ($shares as $share_item) {
                  $shared_user = [
                     'email' => $share_item->email,
                     'status' => $share_item->status,
                     'date_added' => $share_item->created_at,
                  ];
                  if ($share_item->status === 'accepted' && $share_item->user_id) {
                     $user = get_userdata($share_item->user_id);
                     $shared_user['name'] = $user ? $user->display_name : 'Unknown';
                     $shared_user['permission_type'] = $share_item->permission_type;
                  }
                  $shared_users[] = $shared_user;
               }
            }
            return [
               'success' => true,
               'list' => [
                  'id' => (int)$list['id'],
                  'name' => $list['name'],
                  'description' => $list['description'] ?? '',
                  'created_at' => $list['created_at'],
                  'is_owner' => $is_owner,
                  'is_shared' => !$is_owner,
                  'items' => $this->formatItems($formatted_items),
                  'shared_users' => $shared_users
               ]
            ];
         } catch (Exception $e) {
            $this->logError('getListDetails', [
               'error' => $e->getMessage(),
               'user_id' => $user_id,
               'list_id' => $list_id
            ]);
            return [
               'success' => false,
               'message' => 'Error retrieving list details'
            ];
         }
      });
   }
   /**
    * Process favourite notes update
    */
   protected function processNote(int $user_id, array $data): array
   {
      $target_ids = isset($data['target_id'])
         ? array_map('absint', explode(',', $data['target_id']))
         : [];
      $notes = sanitize_textarea_field($data['notes'] ?? '');
      if (empty($target_ids)) {
         return ['success' => false, 'result' => 'No target IDs provided'];
      }
      return $this->favourites->transaction(function ($table) use ($user_id, $target_ids, $notes) {
         $results = [];
         foreach ($target_ids as $target_id) {
            if ($target_id <= 0) {
               $results[] = [
                  'success' => false,
                  'target_id' => $target_id,
                  'message' => 'Invalid target ID'
               ];
               continue;
            }
            $favourite = $table->where([
               'user_id' => $user_id,
               'target_id' => $target_id
            ])->first();
            if (!$favourite) {
               $results[] = [
                  'success' => false,
                  'target_id' => $target_id,
                  'message' => 'Favourite not found'
               ];
               continue;
            }
            $table->where(['id' => $favourite->id])->updateResults([
               'notes' => $notes
            ]);
            $results[] = [
               'success' => true,
               'action' => 'updated_notes',
               'favourite_id' => $favourite->id,
               'target_id' => $target_id
            ];
         }
         return [
            'success' => true,
            'result' => $results
         ];
        } catch (Exception $e) {
            // Something went wrong, roll back changes
            $wpdb->query('ROLLBACK');
      });
   }
            JVB()->error()->log(
                'favourites',
                'Notes update failed: ' . $e->getMessage(),
                [
                    'user_id' => $user_id,
                    'target_id' => $data['target_id'] ?? null
                ],
                'error'
            );
   /**
    * Update list
    */
   protected function updateList(int $user_id, array $data): array
   {
      $list_id = absint($data['list_id'] ?? 0);
            return [
                'success' => false,
                'result' => $e->getMessage(),
            ];
        }
    }
      if (!$list_id) {
         return ['success' => false, 'result' => 'List ID is required'];
      }
    /**
     * Process list creation
     *
     * @param int $user_id User ID
     * @param array $data Operation data
     * @return array Operation result
     */
    protected function processListCreate(int $user_id, array $data):array
    {
        global $wpdb;
      try {
         // Verify ownership
         $is_owner = $this->lists->where([
            'id' => $list_id,
            'user_id' => $user_id
         ])->existsInQuery();
        // Start transaction
        $wpdb->query('START TRANSACTION');
         if (!$is_owner) {
            return [
               'success' => false,
               'result' => 'You do not have permission to update this list'
            ];
         }
        try {
            $name = sanitize_text_field($data['name']);
            $description = sanitize_textarea_field($data['description'] ?? '');
            $items = $data['items'] ?? [];
         // Build update data
         $update_data = [];
            // Fire pre-create action
            do_action('jvb_before_list_create', $user_id, $name, $description);
         if (isset($data['name'])) {
            $update_data['name'] = sanitize_text_field($data['name']);
         }
            $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
         if (isset($data['description'])) {
            $update_data['description'] = sanitize_textarea_field($data['description']);
         }
            // Create the list
            $inserted = $wpdb->insert(
                $lists_table,
                [
                    'user_id' => $user_id,
                    'name' => $name,
                    'description' => $description,
                    'created_at' => current_time('mysql'),
                    'updated_at' => current_time('mysql')
                ],
                ['%d', '%s', '%s', '%s', '%s']
            );
         if (empty($update_data)) {
            return [
               'success' => true,
               'result' => 'No changes to update'
            ];
         }
            if (!$inserted) {
                throw new Exception($wpdb->last_error);
            }
         // Update the list
         $this->lists->where(['id' => $list_id])->updateResults($update_data);
            $list_id = $wpdb->insert_id;
            // Add items if any, but limit batch size
            $added_count = 0;
            $batch_size = 50; // Process in batches of 50
            if (!empty($items)) {
                $item_batches = array_chunk($items, $batch_size);
                foreach ($item_batches as $batch) {
                    $result = $this->addItemsToList($list_id, $batch, $user_id);
                    $batch_added = $result['added'];
                    $added_count += $batch_added;
                    // Give the server a small break between large batches
                    if (count($items) > $batch_size) {
                        usleep(50000); // 50ms pause
                    }
                }
            }
            // Commit transaction
            $wpdb->query('COMMIT');
            // Fire post-create action
            do_action('jvb_after_list_create', $user_id, $list_id, $name, $added_count);
            return [
                'success' => true,
            'result' => [
               'list_id' => $list_id,
                  'name' => $name,
               'added_items' => $added_count,
               'message' => sprintf('Created list "%s" with %d items', $name, $added_count)
            ]
            ];
        } catch (Exception $e) {
            // Rollback on error
            $wpdb->query('ROLLBACK');
            JVB()->error()->log(
                'favourites',
                'Error creating list: ' . $e->getMessage(),
                ['user_id' => $user_id, 'name' => $data['name'] ?? ''],
                'error'
            );
            return [
                'success' => false,
                'result' => $e->getMessage()
            ];
        }
    }
    /**
     * Process adding items to a list
     *
     * @param int $user_id User ID
     * @param array $data Operation data
     * @return array Operation result
     */
    protected function processAddToList(int $user_id, array $data): array
    {
        global $wpdb;
        // Start transaction
        $wpdb->query('START TRANSACTION');
        try {
            $list_ids = explode(',', $data['list_id']);
            $items = $data['items'] ?? [];
            if (empty($items)) {
                throw new Exception('Items array is required');
            }
            $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
            $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
            $results = [];
            $total_added = 0;
            $all_errors = [];
            // Process each list
            foreach ($list_ids as $list_id) {
                $list_id = (int) $list_id;
                if (!$list_id) {
                    $all_errors[] = ['message' => 'Invalid list ID', 'list_id' => $list_id];
                    continue;
                }
                // Check permission - either owner or has edit permission
                $has_permission = $wpdb->get_var($wpdb->prepare(
                    "SELECT 1 FROM {$lists_table} WHERE id = %d AND user_id = %d
                 UNION
                 SELECT 1 FROM {$shares_table}
                 WHERE list_id = %d AND user_id = %d AND permission_type = 'edit'
                 LIMIT 1",
                    $list_id,
                    $user_id,
                    $list_id,
                    $user_id
                ));
                if (!$has_permission) {
                    $all_errors[] = [
                        'message' => 'You do not have permission to add items to this list',
                        'list_id' => $list_id
                    ];
                    continue;
                }
                // Call the optimized helper method to add items
                $add_result = $this->addItemsToList($list_id, $items, $user_id);
                // Track results
                $total_added += $add_result['added'];
                if (!empty($add_result['errors'])) {
                    $all_errors = array_merge($all_errors, $add_result['errors']);
                }
                $results[] = [
                    'list_id' => $list_id,
                    'added_count' => $add_result['added']
                ];
            }
            // If we've had no successful additions and only errors, throw an exception
            if ($total_added == 0 && !empty($all_errors)) {
                throw new Exception('Failed to add any items: ' . json_encode($all_errors));
            }
            // Commit the transaction
            $wpdb->query('COMMIT');
            // Invalidate relevant caches
            CacheManager::invalidateGroup('favourites_lists');
            return [
                'success' => true,
                'results' => [
               'success'   => $results,
                  'added_count' => $total_added,
                  'errors' => $all_errors
            ]
            ];
        } catch (Exception $e) {
            // Something went wrong, roll back changes
            $wpdb->query('ROLLBACK');
            JVB()->error()->log(
                'favourites',
                'Error adding items to list: ' . $e->getMessage(),
                ['user_id' => $user_id, 'list_ids' => $data['list_id'] ?? 0],
                'error'
            );
            return [
                'success' => false,
                'result' => $e->getMessage()
            ];
        }
    }
    /**
     * Helper method to add items to a list with improved performance and error handling
     *
     * @param int $list_id List ID
     * @param array $items Items to add
     * @param int $user_id User ID
     * @return array Success details with counts and errors
     */
    protected function addItemsToList(int $list_id, array $items, int $user_id)
    {
        if (empty($items)) {
            return ['added' => 0, 'errors' => []];
        }
        global $wpdb;
        $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
        $added = 0;
        $errors = [];
        try {
            // Group items by type for more efficient processing
            $items_by_type = [];
            foreach ($items as $item) {
                if (empty($item['type']) || !isset($item['target_id'])) {
                    $errors[] = ['message' => 'Item missing type or target_id', 'item' => $item];
                    continue;
                }
                $type = isset($item['type']) && strpos($item['type'], BASE) !== 0
                    ? BASE . $item['type']
                    : $item['type'];
                if (!isset($items_by_type[$type])) {
                    $items_by_type[$type] = [];
                }
                $items_by_type[$type][] = (int)$item['target_id'];
            }
            // Process each type in bulk
            foreach ($items_by_type as $type => $target_ids) {
                // Find existing items to avoid duplicates
                $placeholders = implode(',', array_fill(0, count($target_ids), '%d'));
                $query_params = array_merge([$list_id], $target_ids);
                array_unshift($query_params, $type);
                $existing_query = $wpdb->prepare(
                    "SELECT item_id FROM {$items_table}
                WHERE list_id = %d AND item_type = %s AND item_id IN ({$placeholders})",
                    $query_params
                );
                $existing_ids = $wpdb->get_col($existing_query);
                $existing_ids_map = array_flip($existing_ids);
                // Get favourite IDs in bulk
                $favourites_table = $wpdb->prefix . BASE . "favourites";
                $fav_query = $wpdb->prepare(
                    "SELECT id, target_id FROM {$favourites_table}
                WHERE user_id = %d AND type = %s AND target_id IN ({$placeholders})",
                    array_merge([$user_id, $type], $target_ids)
                );
                $fav_results = $wpdb->get_results($fav_query);
                $fav_id_map = [];
                foreach ($fav_results as $fav) {
                    $fav_id_map[$fav->target_id] = $fav->id;
                }
                // Prepare bulk insert
                $now = current_time('mysql');
                $insert_values = [];
                $insert_placeholders = [];
                foreach ($target_ids as $target_id) {
                    // Skip if already in list
                    if (isset($existing_ids_map[$target_id])) {
                        continue;
                    }
                    $fav_id = $fav_id_map[$target_id] ?? null;
                    $insert_values[] = $list_id;
                    $insert_values[] = $type;
                    $insert_values[] = $target_id;
                    $insert_values[] = $fav_id;
                    $insert_values[] = $now;
                    $insert_placeholders[] = "(%d, %s, %d, %d, %s)";
                }
                // Perform bulk insert if there are items to add
                if (!empty($insert_placeholders)) {
                    $insert_query = "INSERT INTO {$items_table}
                                (list_id, item_type, item_id, favourite_id, added_at)
                                VALUES " . implode(',', $insert_placeholders);
                    $result = $wpdb->query($wpdb->prepare($insert_query, $insert_values));
                    if ($result === false) {
                        throw new Exception($wpdb->last_error);
                    }
                    $added += $result;
                }
            }
            // Update list modified timestamp
            $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
            $wpdb->update(
                $lists_table,
                ['updated_at' => current_time('mysql')],
                ['id' => $list_id],
                ['%s'],
                ['%d']
            );
            return ['added' => $added, 'errors' => $errors];
        } catch (Exception $e) {
            // Rollback transaction on error
            $wpdb->query('ROLLBACK');
            JVB()->error()->log(
                'favourites',
                'Error adding items to list: ' . $e->getMessage(),
                ['user_id' => $user_id, 'list_id' => $list_id, 'items_count' => count($items)],
                'error'
            );
            $errors[] = ['message' => $e->getMessage()];
            return ['added' => 0, 'errors' => $errors];
        }
    }
    /**
     * Process list update
     *
     * @param int $user_id User ID
     * @param array $data Operation data
     * @return array Operation result
     */
    protected function processUpdateList(int $user_id, array $data):array
    {
        $list_id = intval($data['list_id'] ?? 0);
        if (!$list_id) {
            return [
                'success' => false,
                'result' => 'List ID is required'
            ];
        }
        try {
            global $wpdb;
            $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
            // Verify ownership
            $is_owner = $wpdb->get_var($wpdb->prepare(
                "SELECT 1 FROM {$lists_table}
             WHERE id = %d AND user_id = %d",
                $list_id,
                $user_id
            ));
            if (!$is_owner) {
                return [
                    'success' => false,
                    'result' => 'You do not have permission to update this list'
                ];
            }
            // Build update data
            $update_data = [];
            $update_format = [];
            if (isset($data['name'])) {
                $update_data['name'] = sanitize_text_field($data['name']);
                $update_format[] = '%s';
            }
            if (isset($data['description'])) {
                $update_data['description'] = sanitize_textarea_field($data['description']);
                $update_format[] = '%s';
            }
            if (empty($update_data)) {
                return [
                    'success' => true,
                    'result' => 'No changes to update'
                ];
            }
            // Add updated timestamp
            $update_data['updated_at'] = current_time('mysql');
            $update_format[] = '%s';
            $updated = $wpdb->update(
                $lists_table,
                $update_data,
                ['id' => $list_id, 'user_id' => $user_id],
                $update_format,
                ['%d', '%d']
            );
            if ($updated === false) {
                throw new Exception($wpdb->last_error);
            }
            return [
                'success' => true,
                'result'   => [
         return [
            'success' => true,
            'result' => [
               'message' => 'List updated successfully',
                  'list_id' => $list_id,
                  'updates' => array_keys($update_data)
               'list_id' => $list_id,
               'updates' => array_keys($update_data)
            ]
            ];
        } catch (Exception $e) {
            JVB()->error()->log(
                'favourites',
                'Error updating list: ' . $e->getMessage(),
                ['user_id' => $user_id, 'list_id' => $list_id],
                'error'
            );
         ];
            return [
                'success' => false,
                'result' => $e->getMessage()
            ];
        }
    }
      } catch (Exception $e) {
         $this->logError('updateList', [
            'error' => $e->getMessage(),
            'user_id' => $user_id,
            'list_id' => $list_id
         ]);
    /**
     * Process list deletion
     *
     * @param int $user_id User ID
     * @param array $data Operation data
     * @return array Operation result
     */
    protected function processListDeletion(int $user_id, array $data):array
    {
        $list_id = intval($data['list_id'] ?? 0);
         return [
            'success' => false,
            'result' => $e->getMessage()
         ];
      }
   }
        if (!$list_id) {
            return [
                'success' => false,
                'result' => 'List ID is required'
            ];
        }
   /**
    * Delete list
    */
   protected function deleteList(int $user_id, array $data): array
   {
      $list_id = absint($data['list_id'] ?? 0);
        try {
            global $wpdb;
            $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
      if (!$list_id) {
         return ['success' => false, 'result' => 'List ID is required'];
      }
            // Verify ownership
            $is_owner = $wpdb->get_var($wpdb->prepare(
                "SELECT 1 FROM {$lists_table}
             WHERE id = %d AND user_id = %d",
                $list_id,
                $user_id
            ));
      return $this->lists->transaction(function ($table) use ($user_id, $list_id) {
         // Verify ownership
         $is_owner = $table->where([
            'id' => $list_id,
            'user_id' => $user_id
         ])->existsInQuery();
            if (!$is_owner) {
                return [
                    'success' => false,
                    'result' => 'You do not have permission to delete this list'
                ];
            }
         if (!$is_owner) {
            throw new Exception('You do not have permission to delete this list');
         }
            // Start transaction for cascading deletes
            $wpdb->query('START TRANSACTION');
         // Delete related data (foreign keys should handle this, but being explicit)
         $this->listItems->where(['list_id' => $list_id])->deleteResults();
         $this->listShares->where(['list_id' => $list_id])->deleteResults();
            // Delete from all related tables
            $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
            $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
            $pending_shares_table = $wpdb->prefix . BASE . 'favourites_pending_shares';
         // Delete the list
         $table->where(['id' => $list_id])->deleteResults();
            $wpdb->delete($items_table, ['list_id' => $list_id], ['%d']);
            $wpdb->delete($shares_table, ['list_id' => $list_id], ['%d']);
            $wpdb->delete($pending_shares_table, ['list_id' => $list_id], ['%d']);
            // Delete the list itself
            $deleted = $wpdb->delete(
                $lists_table,
                ['id' => $list_id, 'user_id' => $user_id],
                ['%d', '%d']
            );
            if ($deleted === false) {
                throw new Exception($wpdb->last_error);
            }
            $wpdb->query('COMMIT');
            return [
                'success' => true,
                'result'   => [
         return [
            'success' => true,
            'result' => [
               'message' => 'List deleted successfully',
                  'list_id' => $list_id
               'list_id' => $list_id
            ]
            ];
        } catch (Exception $e) {
            $wpdb->query('ROLLBACK');
         ];
      });
   }
            JVB()->error()->log(
                'favourites',
                'Error deleting list: ' . $e->getMessage(),
                ['user_id' => $user_id, 'list_id' => $list_id],
                'error'
            );
   /**
    * Add items to list
    */
   protected function addToList(int $user_id, array $data): array
   {
      $list_ids = isset($data['list_id'])
         ? array_map('absint', explode(',', $data['list_id']))
         : [];
      $items = $data['items'] ?? [];
            return [
                'success' => false,
                'result' => $e->getMessage()
            ];
        }
    }
      if (empty($list_ids) || empty($items)) {
         return ['success' => false, 'result' => 'List ID and items are required'];
      }
    /**
     * Process removing items from a list
     *
     * @param int $user_id User ID
     * @param array $data Operation data
     * @return array Operation result
     */
    protected function removeFromList(int $user_id, array $data):array
    {
        $list_id = intval($data['list_id'] ?? 0);
        $items = $data['items'] ?? [];
      return $this->listItems->transaction(function ($table) use ($user_id, $list_ids, $items) {
         $results = [];
         $total_added = 0;
         $all_errors = [];
        if (!$list_id || empty($items)) {
            return [
                'success' => false,
                'result' => 'List ID and items are required'
            ];
        }
         foreach ($list_ids as $list_id) {
            if (!$list_id) {
               $all_errors[] = ['message' => 'Invalid list ID', 'list_id' => $list_id];
               continue;
            }
        try {
            global $wpdb;
            $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
            $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
            $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
            // Check permission - either owner or has edit permission
            $is_owner = $this->lists->where([
               'id' => $list_id,
               'user_id' => $user_id
            ])->existsInQuery();
            // Check permission - either owner or has edit permission
            $has_permission = $wpdb->get_var($wpdb->prepare(
                "SELECT 1 FROM {$lists_table} WHERE id = %d AND user_id = %d
             UNION
             SELECT 1 FROM {$shares_table}
             WHERE list_id = %d AND user_id = %d AND permission_type = 'edit'
             LIMIT 1",
                $list_id,
                $user_id,
                $list_id,
                $user_id
            ));
            $has_edit = $this->listShares->where([
               'list_id' => $list_id,
               'user_id' => $user_id,
               'permission_type' => 'edit',
               'status' => 'accepted'
            ])->existsInQuery();
            if (!$has_permission) {
                return [
                    'success' => false,
                    'result' => 'You do not have permission to remove items from this list'
                ];
            }
            if (!$is_owner && !$has_edit) {
               $all_errors[] = [
                  'message' => 'You do not have permission to add items to this list',
                  'list_id' => $list_id
               ];
               continue;
            }
            // Remove items
            $removed = 0;
            // Add items to this list
            $add_result = $this->addItemsToList($list_id, $items, $user_id);
            $total_added += $add_result['added'];
            foreach ($items as $item) {
                if (empty($item['type']) || !isset($item['target_id'])) {
                    continue;
                }
            if (!empty($add_result['errors'])) {
               $all_errors = array_merge($all_errors, $add_result['errors']);
            }
                $type = isset($item['type']) && strpos($item['type'], BASE) !== 0
                    ? BASE . $item['type']
                    : $item['type'];
            $results[] = [
               'list_id' => $list_id,
               'added_count' => $add_result['added']
            ];
         }
                $deleted = $wpdb->delete(
                    $items_table,
                    [
                        'list_id' => $list_id,
                        'item_type' => $type,
                        'item_id' => intval($item['target_id'])
                    ],
                    ['%d', '%s', '%d']
                );
         if ($total_added == 0 && !empty($all_errors)) {
            throw new Exception('Failed to add any items');
         }
                if ($deleted) {
                    $removed += $deleted;
                }
            }
         return [
            'success' => true,
            'result' => [
               'lists' => $results,
               'total_added' => $total_added,
               'errors' => $all_errors
            ]
         ];
      });
   }
            return [
                'success' => true,
                'result'   => [
   /**
    * Remove items from list
    */
   protected function removeFromList(int $user_id, array $data): array
   {
      $list_id = absint($data['list_id'] ?? 0);
      $items = $data['items'] ?? [];
      if (!$list_id || empty($items)) {
         return ['success' => false, 'result' => 'List ID and items are required'];
      }
      try {
         // Check permission
         $is_owner = $this->lists->where([
            'id' => $list_id,
            'user_id' => $user_id
         ])->existsInQuery();
         $has_edit = $this->listShares->where([
            'list_id' => $list_id,
            'user_id' => $user_id,
            'permission_type' => 'edit',
            'status' => 'accepted'
         ])->existsInQuery();
         if (!$is_owner && !$has_edit) {
            return [
               'success' => false,
               'result' => 'You do not have permission to remove items from this list'
            ];
         }
         // Remove items
         $removed = 0;
         foreach ($items as $item) {
            if (empty($item['type']) || !isset($item['target_id'])) {
               continue;
            }
            $type = str_starts_with($item['type'], BASE)
               ? $item['type']
               : BASE . $item['type'];
            $deleted = $this->listItems->where([
               'list_id' => $list_id,
               'item_type' => $type,
               'item_id' => absint($item['target_id'])
            ])->deleteResults();
            if ($deleted) {
               $removed += $deleted;
            }
         }
         return [
            'success' => true,
            'result' => [
               'message' => "{$removed} items removed from list",
               'list_id' => $list_id,
               'removed_count' => $removed
            ]
            ];
        } catch (Exception $e) {
            JVB()->error()->log(
                'favourites',
                'Error removing items from list: ' . $e->getMessage(),
                ['user_id' => $user_id, 'list_id' => $list_id],
                'error'
            );
         ];
            return [
                'success' => false,
                'result' => $e->getMessage()
            ];
        }
    }
      } catch (Exception $e) {
         $this->logError('removeFromList', [
            'error' => $e->getMessage(),
            'user_id' => $user_id,
            'list_id' => $list_id
         ]);
    /**
     * Process sharing a list
     *
     * @param int $user_id User ID
     * @param array $data Operation data
     * @return array Operation result
     */
    protected function shareList(int $user_id, array $data):array
    {
        global $wpdb;
        $result = null;
         return [
            'success' => false,
            'result' => $e->getMessage()
         ];
      }
   }
        // Start transaction
        $wpdb->query('START TRANSACTION');
   /**
    * Share list with another user
    */
   protected function shareList(int $user_id, array $data): array
   {
      $list_id = absint($data['list_id'] ?? 0);
      $email = sanitize_email($data['email'] ?? '');
      $permission_type = in_array($data['permission_type'] ?? '', ['view', 'edit'])
         ? $data['permission_type']
         : 'view';
        try {
            $list_id = intval($data['list_id'] ?? 0);
            $email = sanitize_email($data['email'] ?? '');
            $permission_type = in_array($data['permission_type'] ?? '', ['view', 'edit'])
                ? $data['permission_type']
                : 'view';
      if (!$list_id || !$email) {
         return ['success' => false, 'result' => 'List ID and email are required'];
      }
            if (!$list_id || !$email) {
                throw new Exception('List ID and email are required');
            }
      return $this->listShares->transaction(function ($table) use ($user_id, $list_id, $email, $permission_type) {
         // Verify ownership
         $list = $this->lists->where([
            'id' => $list_id,
            'user_id' => $user_id
         ])->first();
            $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
            $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
         if (!$list) {
            throw new Exception('You do not have permission to share this list');
         }
            // Verify ownership
            $list = $wpdb->get_row($wpdb->prepare(
                "SELECT l.*, u.display_name as owner_name
             FROM {$lists_table} l
             JOIN {$wpdb->users} u ON l.user_id = u.ID
             WHERE l.id = %d AND l.user_id = %d",
                $list_id,
                $user_id
            ));
         // Get owner details
         $owner = get_userdata($user_id);
         $owner_name = $owner ? $owner->display_name : 'Someone';
            if (!$list) {
                throw new Exception('You do not have permission to share this list');
            }
         // Look up user by email
         $share_user = get_user_by('email', $email);
            // Look up user by email
            $share_user = get_user_by('email', $email);
         if ($share_user) {
            // User exists - check for existing share
            $existing = $table->where([
               'list_id' => $list_id,
               'user_id' => $share_user->ID
            ])->first();
            if ($share_user) {
                // User exists - check if they already have access
                $existing_share = $wpdb->get_row($wpdb->prepare(
                    "SELECT * FROM {$shares_table}
                 WHERE list_id = %d AND (user_id = %d OR email = %s)",
                    $list_id,
                    $share_user->ID,
                    $email
                ));
            if ($existing) {
               // Update if different
               if ($existing->permission_type !== $permission_type || $existing->status !== 'accepted') {
                  $table->where(['id' => $existing->id])->updateResults([
                     'permission_type' => $permission_type,
                     'status' => 'accepted',
                  ]);
                if ($existing_share) {
                    // Update permission if it's different
                    if ($existing_share->permission_type !== $permission_type ||
                        $existing_share->status !== 'accepted') {
                        $wpdb->update(
                            $shares_table,
                            [
                                'permission_type' => $permission_type,
                                'status' => 'accepted',
                                'user_id' => $share_user->ID,
                                'updated_at' => current_time('mysql')
                            ],
                            ['id' => $existing_share->id]
                        );
                  return [
                     'success' => true,
                     'result' => [
                        'action' => 'updated',
                        'message' => "Updated sharing permissions for {$email}",
                     ]
                  ];
               }
                        if ($wpdb->last_error) {
                            throw new Exception($wpdb->last_error);
                        }
                        $result = [
                            'success' => true,
                            'action' => 'updated',
                            'message' => "Updated sharing permissions for {$email}",
                            'user_id' => $share_user->ID,
                            'email' => $email,
                            'permission_type' => $permission_type
                        ];
                    } else {
                        $result = [
                            'success' => true,
                            'action' => 'already_shared',
                            'message' => "List is already shared with {$email}",
                            'user_id' => $share_user->ID,
                            'email' => $email,
                            'permission_type' => $permission_type
                        ];
                    }
                } else {
                    // Add new share for existing user
                    $wpdb->insert(
                        $shares_table,
                        [
                            'list_id' => $list_id,
                            'user_id' => $share_user->ID,
                            'email' => $email,
                            'permission_type' => $permission_type,
                            'status' => 'accepted',
                            'created_at' => current_time('mysql'),
                            'updated_at' => current_time('mysql')
                        ],
                        ['%d', '%d', '%s', '%s', '%s', '%s', '%s']
                    );
                    if ($wpdb->last_error) {
                        throw new Exception($wpdb->last_error);
                    }
                    // Send notification to user
                    JVB()->notification()->addNotification(
                        $share_user->ID, // Recipient
                        'list_shared',
                        $user_id, // Action user ID
                        sprintf('%s shared a favorites list with you: "%s"', jvbShareName($user_id), $list->name),
                        $list_id,
                        'favourites_list',
                        [
                            'list_id' => $list_id,
                            'list_name' => $list->name,
                            'permission_type' => $permission_type
                        ]
                    );
                    $result = [
                        'success' => true,
                        'result' => [
                     'message' => "List shared with {$email}",
                     'user_id' => $share_user->ID,
                     'email' => $email,
                     'permission_type' => $permission_type
               return [
                  'success' => true,
                  'result' => [
                     'action' => 'already_shared',
                     'message' => "List is already shared with {$email}",
                  ]
                    ];
                }
            } else {
                // User doesn't exist - check for existing pending invitation
                $existing_pending = $wpdb->get_var($wpdb->prepare(
                    "SELECT id FROM {$shares_table}
                 WHERE list_id = %d AND email = %s AND status = 'pending'",
                    $list_id,
                    $email
                ));
               ];
            }
                if ($existing_pending) {
                    $result = [
                        'success' => true,
                  'result' => [
                     'action' => 'already_pending',
                     'message' => "Invitation already sent to {$email}",
                     'email' => $email
            // Create new share
            $table->create([
               'list_id' => $list_id,
               'user_id' => $share_user->ID,
               'email' => $email,
               'permission_type' => $permission_type,
               'status' => 'accepted'
            ]);
            // Send notification
            JVB()->notification()->addNotification(
               $share_user->ID,
               'list_shared',
               $user_id,
               sprintf('%s shared a favorites list with you: "%s"', $owner_name, $list->name),
               $list_id,
               'favourites_list',
               [
                  'list_id' => $list_id,
                  'list_name' => $list->name,
                  'permission_type' => $permission_type
               ]
            );
            return [
               'success' => true,
               'result' => [
                  'action' => 'shared',
                  'message' => "List shared with {$email}",
               ]
            ];
         }
         // User doesn't exist - create pending invitation
         $existing_pending = $table->where([
            'list_id' => $list_id,
            'email' => $email,
            'status' => 'pending'
         ])->first();
         if ($existing_pending) {
            return [
               'success' => true,
               'result' => [
                  'action' => 'already_pending',
                  'message' => "Invitation already sent to {$email}",
               ]
            ];
         }
         // Create pending share with invitation token
         $token = wp_generate_password(32, false);
         $table->create([
            'list_id' => $list_id,
            'email' => $email,
            'permission_type' => $permission_type,
            'status' => 'pending',
            'invitation_token' => $token,
            'user_id' => 0 // Will be set when they accept
         ]);
         // Send invitation email
         $this->sendListInviteEmail($email, [
            'list_id' => $list_id,
            'list_name' => $list->name,
            'token' => $token,
            'owner_name' => $owner_name,
            'user_id' => $user_id
         ]);
         return [
            'success' => true,
            'result' => [
               'action' => 'invitation_sent',
               'message' => "Invitation sent to {$email}",
            ]
         ];
      });
   }
   /**
    * Unshare list
    */
   protected function unshareList(int $user_id, array $data): array
   {
      $list_id = absint($data['list_id'] ?? 0);
      $email = sanitize_email($data['email'] ?? '');
      if (!$list_id || !$email) {
         return ['success' => false, 'result' => 'List ID and email are required'];
      }
      try {
         // Verify ownership
         $is_owner = $this->lists->where([
            'id' => $list_id,
            'user_id' => $user_id
         ])->existsInQuery();
         if (!$is_owner) {
            return [
               'success' => false,
               'result' => 'You do not have permission to manage shares for this list'
            ];
         }
         // Find share by email
         $existing_share = $this->listShares->where([
            'list_id' => $list_id,
            'email' => $email
         ])->first();
         if (!$existing_share) {
            return [
               'success' => false,
               'result' => "No active share or invitation found for {$email}"
            ];
         }
         // Update status to revoked (or just delete)
         $this->listShares->where(['id' => $existing_share->id])->updateResults([
            'status' => 'revoked'
         ]);
         // Notify user if registered and was accepted
         if ($existing_share->status === 'accepted' && $existing_share->user_id) {
            $list = $this->lists->where(['id' => $list_id])->first();
            if ($list) {
               JVB()->notification()->addNotification(
                  $existing_share->user_id,
                  'list_share_revoked',
                  $user_id,
                  sprintf('Your access to the list "%s" has been revoked', $list->name),
                  $list_id,
                  'favourites_list',
                  [
                     'list_id' => $list_id,
                     'list_name' => $list->name
                  ]
                    ];
                } else {
                    // Generate invitation token
                    $token = wp_generate_password(32, false);
               );
            }
         }
                    // Create pending share
                    $wpdb->insert(
                        $shares_table,
                        [
                            'list_id' => $list_id,
                            'email' => $email,
                            'permission_type' => $permission_type,
                            'status' => 'pending',
                            'invitation_token' => $token,
                            'created_at' => current_time('mysql'),
                            'updated_at' => current_time('mysql')
                        ],
                        ['%d', '%s', '%s', '%s', '%s', '%s', '%s']
                    );
         $action = $existing_share->status === 'accepted' ? 'unshared' : 'invitation_cancelled';
         $message = $existing_share->status === 'accepted'
            ? "Removed {$email}'s access to list"
            : "Cancelled invitation to {$email}";
                    if ($wpdb->last_error) {
                        throw new Exception($wpdb->last_error);
                    }
                    // Send invitation email
                    $this->sendListInviteEmail($email, [
                        'list_id' => $list_id,
                        'list_name' => $list->name,
                        'token' => $token,
                        'owner_name' => $list->owner_name,
                        'user_id' => $user_id
                    ]);
                    $result = [
                        'success' => true,
                  'result' => [
                     'action' => 'invitation_sent',
                     'message' => "Invitation sent to {$email}",
                     'email' => $email
                  ]
                    ];
                }
            }
            // All operations successful, commit the transaction
            $wpdb->query('COMMIT');
            // Return the result that was set earlier
            return $result;
        } catch (Exception $e) {
            // Something went wrong, roll back changes
            $wpdb->query('ROLLBACK');
            JVB()->error()->log(
                'favourites',
                'Error sharing list: ' . $e->getMessage(),
                ['user_id' => $user_id, 'list_id' => $data['list_id'] ?? 0, 'email' => $data['email'] ?? ''],
                'error'
            );
            return [
                'success' => false,
                'result' => $e->getMessage()
            ];
        }
    }
    /**
     * Process unsharing a list
     *
     * @param int $user_id User ID
     * @param array $data Operation data
     * @return array Operation result
     */
    protected function unshareList(int $user_id, array $data):array
    {
        $list_id = intval($data['list_id'] ?? 0);
        $email = sanitize_email($data['email'] ?? '');
        if (!$list_id || !$email) {
            return [
                'success' => false,
                'result' => 'List ID and email are required'
            ];
        }
        try {
            global $wpdb;
            $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
            $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
            // Verify ownership
            $is_owner = $wpdb->get_var($wpdb->prepare(
                "SELECT 1 FROM {$lists_table}
             WHERE id = %d AND user_id = %d",
                $list_id,
                $user_id
            ));
            if (!$is_owner) {
                return [
                    'success' => false,
                    'result' => 'You do not have permission to manage shares for this list'
                ];
            }
            // Look for any share with this email, regardless of status
            $existing_share = $wpdb->get_row($wpdb->prepare(
                "SELECT * FROM {$shares_table}
             WHERE list_id = %d AND email = %s",
                $list_id,
                $email
            ));
            if (!$existing_share) {
                // Also check by user_id if it's a registered user's email
                $share_user = get_user_by('email', $email);
                if ($share_user) {
                    $existing_share = $wpdb->get_row($wpdb->prepare(
                        "SELECT * FROM {$shares_table}
                     WHERE list_id = %d AND user_id = %d",
                        $list_id,
                        $share_user->ID
                    ));
                }
            }
            if (!$existing_share) {
                return [
                    'success' => false,
                    'result' => "No active share or invitation found for {$email}"
                ];
            }
            // For active shares, update status to 'revoked'
            // For pending invitations, also update status to 'revoked'
            $updated = $wpdb->update(
                $shares_table,
                [
                    'status' => 'revoked',
                    'updated_at' => current_time('mysql')
                ],
                ['id' => $existing_share->id],
                ['%s', '%s'],
                ['%d']
            );
            if ($updated === false) {
                throw new Exception($wpdb->last_error);
            }
            // Determine the appropriate message based on previous status
            if ($existing_share->status === 'accepted') {
                $action = 'unshared';
                $message = "Removed {$email}'s access to list";
                // Send notification to user if they're registered
                if ($existing_share->user_id) {
                    // Get list details
                    $list = $wpdb->get_row($wpdb->prepare(
                        "SELECT * FROM {$lists_table} WHERE id = %d",
                        $list_id
                    ));
                    if ($list) {
                        JVB()->notification()->addNotification(
                            $existing_share->user_id,
                            'list_share_revoked',
                            $user_id, // Action user ID
                            sprintf('Your access to the list "%s" has been revoked', $list->name),
                            $list_id,
                            'favourites_list',
                            [
                                'list_id' => $list_id,
                                'list_name' => $list->name
                            ]
                        );
                    }
                }
            } else {
                $action = 'invitation_cancelled';
                $message = "Cancelled invitation to {$email}";
            }
            return [
                'success' => true,
            'result' => [
         return [
            'success' => true,
            'result' => [
               'action' => $action,
               'message' => $message,
               'email' => $email
            ]
            ];
         ];
        } catch (Exception $e) {
            JVB()->error()->log(
                'favourites',
                'Error unsharing list: ' . $e->getMessage(),
                ['user_id' => $user_id, 'list_id' => $list_id, 'email' => $email],
                'error'
            );
      } catch (Exception $e) {
         $this->logError('unshareList', [
            'error' => $e->getMessage(),
            'user_id' => $user_id,
            'list_id' => $list_id,
            'email' => $email
         ]);
            return [
                'success' => false,
                'result' => $e->getMessage()
            ];
        }
    }
         return [
            'success' => false,
            'result' => $e->getMessage()
         ];
      }
   }
    /**
     * Send list invitation email
     *
     * @param string $email Recipient email
     * @param array $data Invitation data
     * @return bool Success status
     */
    protected function sendListInviteEmail(string $email, array $data):bool
    {
        $list_name = $data['list_name'];
        $token = $data['token'];
        $owner_name = $data['owner_name'];
   /**
    * Send list invitation email
    */
   protected function sendListInviteEmail(string $email, array $data): bool
   {
      $list_name = $data['list_name'];
      $token = $data['token'];
      $owner_name = $data['owner_name'];
        // Generate invitation URL
        $invite_url = add_query_arg([
            'action' => 'accept_list_invite',
            'token' => $token,
            'list' => $data['list_id']
        ], home_url('/'));
      // Generate invitation URL
      $invite_url = add_query_arg([
         'action' => 'accept_list_invite',
         'token' => $token,
         'list' => $data['list_id']
      ], home_url('/'));
        $inviteButton = sprintf(
            '<p style="text-align: center;"><a href="%s" class="button">Set Your Password</a></p>',
            $invite_url
        );
        $inviteUrl = sprintf(
            '<p style="user-select:all;">%s</p>',
            $invite_url
        );
      $subject = sprintf('%s shared a favourites list with you', $owner_name);
        $subject = sprintf('%s shared a favourites list with you on edmonton.ink', $owner_name);
      $message = sprintf(
         '<p>Hi there,</p>
        <p><strong>%s</strong> has shared their list "<strong>%s</strong>" with you.</p>
        <p>To view this list, click the link below:</p>
        <p><a href="%s">Accept List Invitation</a></p>
        <p>Or copy and paste this link: %s</p>
        <p>If you don\'t have an account, you\'ll be guided through creating one.</p>',
         $owner_name,
         $list_name,
         $invite_url,
         $invite_url
      );
        $message = sprintf(
            '<p>Hi there,</p>
            <p><strong>%s</strong> has shared their list \"<strong>%s</strong>\" with you on edmonton.ink.</p>
            <p>To view this list, click the button below:</p>
            %s
            <p>Or copy and paste this link into your browser:</p>
            %s
            <p>If you don\'t already have an account, you\'ll be guided through creating one.</p>
            %s',
            $owner_name,
            $list_name,
            $inviteButton,
            $inviteUrl,
            jvbSignature()
        );
      return JVB()->email()->sendEmail($email, $subject, $message);
   }
        return jvbMail($email, $subject, $message);
    }
   /**
    * Accept a list share invitation
    */
   protected function acceptListInvitation(string $token, string $email, ?int $user_id = null): array
   {
      if (!$token || !$email) {
         return [
            'success' => false,
            'message' => 'Invalid invitation parameters'
         ];
      }
    /**
     * Accept a list share invitation, handling both registered and non-registered users
     *
     * @param string $token Invitation token
     * @param string $email Email address of the invited user
     * @param int|null $user_id Optional user ID if already registered
     * @return array Result with success status and messages
     */
    public function acceptListInvitation(string $token, string $email, ?int $user_id = null):array
    {
        if (!$token || !$email) {
            return [
                'success' => false,
                'message' => 'Invalid invitation parameters'
            ];
        }
      try {
         // Find the pending invitation
         $invitation = $this->listShares->where([
            'invitation_token' => $token,
            'email' => $email,
            'status' => 'pending'
         ])->first();
        try {
            global $wpdb;
            $shares_table = $wpdb->prefix . BASE . 'favourites_list_shares';
         if (!$invitation) {
            return [
               'success' => false,
               'message' => 'Invalid or expired invitation'
            ];
         }
            // Find the pending invitation
            $invitation = $wpdb->get_row($wpdb->prepare(
                "SELECT * FROM {$shares_table}
             WHERE invitation_token = %s
             AND email = %s
             AND status = 'pending'",
                $token,
                $email
            ));
         // If no user_id provided, check if user exists
         if (!$user_id) {
            $existing_user = get_user_by('email', $email);
            if (!$invitation) {
                return [
                    'success' => false,
                    'message' => 'Invalid or expired invitation'
                ];
            }
            if ($existing_user) {
               $user_id = $existing_user->ID;
            } else {
               // No account - need to register
               $registration_url = add_query_arg([
                  'action' => 'register',
                  'type' => 'favourites',
                  'list_token' => $token,
                  'email' => urlencode($email)
               ], wp_login_url());
            // If no user_id provided, check if a user with this email exists
            if (!$user_id) {
                $existing_user = get_user_by('email', $email);
               return [
                  'success' => false,
                  'message' => 'You need to create an account to access this shared list',
                  'needs_registration' => true,
                  'registration_url' => $registration_url
               ];
            }
         }
                if ($existing_user) {
                    $user_id = $existing_user->ID;
                } else {
                    // No user account exists - create a registration URL with the token embedded
                    $registration_url = add_query_arg([
                        'action' => 'register',
                        'type' => 'favourites',
                        'list_token' => $token,
                        'email' => urlencode($email)
                    ], wp_login_url());
         // Get list details
         $list = $this->lists->where(['id' => $invitation->list_id])->first();
                    return [
                        'success' => false,
                        'message' => 'You need to create an account to access this shared list',
                        'needs_registration' => true,
                        'registration_url' => $registration_url
                    ];
                }
            }
         if (!$list) {
            return [
               'success' => false,
               'message' => 'The shared list no longer exists'
            ];
         }
            // Get the list details for the response
            $lists_table = $wpdb->prefix . BASE . 'favourites_lists';
            $list = $wpdb->get_row($wpdb->prepare(
                "SELECT l.*, u.display_name as owner_name
             FROM {$lists_table} l
             JOIN {$wpdb->users} u ON l.user_id = u.ID
             WHERE l.id = %d",
                $invitation->list_id
            ));
         // Update invitation to accepted
         $this->listShares->where(['id' => $invitation->id])->updateResults([
            'status' => 'accepted',
            'user_id' => $user_id,
         ]);
            if (!$list) {
                return [
                    'success' => false,
                    'message' => 'The shared list no longer exists'
                ];
            }
         // Notify list owner
         $user = get_userdata($user_id);
         $display_name = $user ? $user->display_name : $email;
            // Update the invitation to accepted status
            $updated = $wpdb->update(
                $shares_table,
                [
                    'status' => 'accepted',
                    'user_id' => $user_id,
                    'updated_at' => current_time('mysql')
                ],
                ['id' => $invitation->id],
                ['%s', '%d', '%s'],
                ['%d']
            );
         JVB()->notification()->addNotification(
            $list->user_id,
            'list_share_accepted',
            $user_id,
            sprintf('%s accepted your invitation to the list "%s"', $display_name, $list->name),
            $invitation->list_id,
            'favourites_list',
            [
               'list_id' => $invitation->list_id,
               'list_name' => $list->name,
            ]
         );
            if ($updated === false) {
                throw new Exception($wpdb->last_error);
            }
         return [
            'success' => true,
            'message' => 'List successfully shared with you',
            'list_id' => $invitation->list_id,
            'list_name' => $list->name,
            'permission_type' => $invitation->permission_type
         ];
            // Notify the list owner
            $user = get_userdata($user_id);
            $display_name = $user ? $user->display_name : $email;
            JVB()->notification()->addNotification(
                $list->user_id, // Owner ID
                'list_share_accepted',
                $user_id, // Action user ID
                sprintf('%s accepted your invitation to the list "%s"', $display_name, $list->name),
                $invitation->list_id,
                'favourites_list',
                [
                    'list_id' => $invitation->list_id,
                    'list_name' => $list->name,
                    'user_id' => $user_id,
                    'email' => $email
                ]
            );
      } catch (Exception $e) {
         $this->logError('acceptListInvitation', [
            'error' => $e->getMessage(),
            'token' => $token,
            'email' => $email
         ]);
            return [
                'success' => true,
                'message' => 'List successfully shared with you',
                'list_id' => $invitation->list_id,
                'list_name' => $list->name,
                'permission_type' => $invitation->permission_type
            ];
         return [
            'success' => false,
            'message' => $e->getMessage()
         ];
      }
   }
        } catch (Exception $e) {
            JVB()->error()->log(
                'favourites',
                'Error accepting list invitation: ' . $e->getMessage(),
                ['token' => $token, 'email' => $email],
                'error'
            );
   /**
    * Get the owner ID for a content item
    */
   protected function getContentOwner(string $type, int $target_id): int|array|null
   {
      // For posts
      if (str_contains($type, 'post') || in_array($type, array_keys($this->valid_types))) {
         $post = get_post($target_id);
         return $post ? $post->post_author : null;
      }
            return [
                'success' => false,
                'message' => $e->getMessage()
            ];
        }
    }
      // For terms (shops, etc.)
      $owners = get_term_meta($target_id, BASE . 'owner', true);
      if ($owners) {
         $owner_array = array_map('absint', explode(',', $owners));
         return count($owner_array) === 1 ? $owner_array[0] : $owner_array;
      }
    /**
     * Get count of favourites for an item
     *
     * @param string $type Item type
     * @param int $target_id Item ID
     * @return int Favourite count
     */
    protected function getFavouriteCount(string $type, int $target_id):int
    {
        try {
            global $wpdb;
            $table = $wpdb->prefix . BASE . 'favourites';
      return null;
   }
            return (int)$wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$table}
             WHERE type = %s AND target_id = %d",
                $type,
                $target_id
            ));
        } catch (Exception $e) {
            return 0;
        }
    }
   /**
    * Batch format post-type favourites to reduce queries
    */
   protected function formatPostFavourites(array $items): array
   {
      if (empty($items)) {
         return [];
      }
    /**
     * Update favourite count meta for an item
     *
     * @param string $type Item type
     * @param int $target_id Item ID
     */
    protected function updateFavouriteCount(string $type, int $target_id):void
    {
        if (!isset($this->valid_types[$type])) {
            return;
        }
        try {
            $config = $this->valid_types[$type];
            $count = $this->getFavouriteCount($type, $target_id);
      $formatted = [];
      $post_ids = array_map(fn($item) => (int)$item->target_id, $items);
            if ($config['table'] === 'post') {
                update_post_meta($target_id, $config['count_meta_key'], $count);
            } else {
                update_term_meta($target_id, $config['count_meta_key'], $count);
            }
        } catch (Exception $e) {
            // Log but continue
            JVB()->error()->log(
                'favourites',
                'Error updating favourite count: ' . $e->getMessage(),
                ['type' => $type, 'target_id' => $target_id],
                'warning'
            );
        }
    }
      // Get all posts in one query
      $posts = get_posts([
         'post__in' => $post_ids,
         'post_type' => 'any',
         'posts_per_page' => -1,
         'post_status' => 'any',
      ]);
    /**
     * Notify content owner of new favourite if configured
     *
     * @param string $type Content type
     * @param int $target_id Content ID
     * @param int $user_id User who favourited
     * @return void
     */
    protected function maybeNotifyOwner(string $type, int $target_id, int $user_id):void
    {
        // Skip if this type doesn't need owner notification
        if (!array_key_exists($type, $this->valid_types) || !$this->valid_types[$type]['notify_owner']) {
            return;
        }
      // Create lookup map
      $posts_by_id = [];
      foreach ($posts as $post) {
         $posts_by_id[$post->ID] = $post;
      }
        try {
            $owner_ids = $this->getContentOwner($type, $target_id);
            if ($owner_ids) {
                $owner_ids = (is_array($owner_ids)) ? $owner_ids : [$owner_ids];
                foreach ($owner_ids as $owner_id) {
                    if ($owner_id !== $user_id) {
                        $type_label = str_replace(BASE, '', $type);
                        JVB()->notification()->addNotification(
                            $owner_id,
                            'new_favourite',
                            $user_id, // Action user ID
                            sprintf('%s favourited your %s', jvbShareName($user_id), $type_label),
                            $target_id,
                            $type,
                            [
                                'favourite_user_id' => $user_id,
                                'content_type' => $type,
                                'content_id' => $target_id
                            ]
                        );
                    }
                }
            }
        } catch (Exception $e) {
            // Log but continue
            JVB()->error()->log(
                'favourites',
                'Error notifying owner: ' . $e->getMessage(),
                ['type' => $type, 'target_id' => $target_id, 'user_id' => $user_id],
                'warning'
            );
        }
    }
      // Get thumbnails for artists
      $artist_ids = [];
      foreach ($items as $item) {
         if ($item->type === BASE . 'artist') {
            $artist_ids[] = (int)$item->target_id;
         }
      }
    /**
     * Get the owner ID for a content item
     *
     * @param string $type Content type
     * @param int $target_id Content ID
     * @return int|null Owner ID
     */
    protected function getContentOwner(string $type, int $target_id):int|null
    {
        if (!array_key_exists($type, $this->valid_types)) {
            return null;
        }
      $artist_images = [];
      if (!empty($artist_ids)) {
         global $wpdb;
         $placeholders = implode(',', array_fill(0, count($artist_ids), '%d'));
         $results = $wpdb->get_results($wpdb->prepare(
            "SELECT post_id, meta_value FROM {$wpdb->postmeta}
            WHERE meta_key = %s AND post_id IN ($placeholders)",
            array_merge([BASE . 'image'], $artist_ids)
         ));
        try {
            if ($this->valid_types[$type]['table'] === 'post') {
                $post = get_post($target_id);
                return $post ? $post->post_author : false;
            } elseif ($type === BASE.'shop') {
                return $this->getShopOwner($target_id);
            }
        } catch (Exception $e) {
            return null;
        }
        return null;
    }
    /**
     * Get the owner ID for a shop
     *
     * @param int $shop_id Shop term ID
     * @return int|null Owner ID
     */
    protected function getShopOwner(int $shop_id):array
    {
        // Get shop manager users
        $owners = get_term_meta($shop_id, BASE.'owner', true);
        $owners  = explode(',', $owners);
        $managers = get_term_meta($shop_id, BASE.'managers', true);
        $managers  = explode(',', $managers);
        return array_merge($owners, $managers);
    }
    /**
     * Maintenance method to clean up orphaned favourites
     * Called by scheduled task
     */
    /**
     * Maintenance method to clean up orphaned favourites
     * Scheduled action
     * @return bool
     */
    public function cleanupOrphanedFavourites():bool
    {
        try {
            global $wpdb;
            $table = $wpdb->prefix . BASE.'favourites';
            // Delete favourites for non-existent users
            $wpdb->query("
            DELETE f FROM $table f
            LEFT JOIN {$wpdb->users} u ON f.user_id = u.ID
            WHERE u.ID IS NULL
        ");
            // Delete favourites for non-existent posts
            $post_types = array_map(function ($type) use ($wpdb) {
                return $wpdb->prepare('%s', $type);
            }, array_filter(array_keys($this->valid_types), function ($type) {
                return $type === 'content';
            }));
            if (!empty($post_types)) {
                $post_types_list = implode(',', $post_types);
                $wpdb->query("
                DELETE f FROM $table f
                LEFT JOIN {$wpdb->posts} p ON f.target_id = p.ID
                WHERE f.type IN ($post_types_list)
                AND p.ID IS NULL
            ");
            }
            // Delete favourites for non-existent terms
            $term_types = array_map(function ($type) use ($wpdb) {
                return $wpdb->prepare('%s', $type);
            }, array_filter(array_keys($this->valid_types), function ($type) {
                return $type === 'tax';
            }));
            if (!empty($term_types)) {
                $term_types_list = implode(',', $term_types);
                $wpdb->query("
                DELETE f FROM $table f
                LEFT JOIN {$wpdb->terms} t ON f.target_id = t.term_id
                WHERE f.type IN ($term_types_list)
                AND t.term_id IS NULL
            ");
            }
            return true;
        } catch (Exception $e) {
            JVB()->error()->log(
                'favourites',
                'Error during cleanup: ' . $e->getMessage(),
                [],
                'error'
            );
            return false;
        }
    }
    /**
     * Create a standardized error response
     *
     * @param string $code Error code
     * @param string $message Error message
     * @param int $status HTTP status code
     * @param array $additional_data Additional context data
     * @return WP_REST_Response Formatted error
     */
    protected function createErrorResponse(string $code, string $message, int $status = 400, array $additional_data = []):WP_REST_Response
    {
        $error = new WP_Error(
            $code,
            __($message, 'jvb'),
            ['status' => $status]
        );
         foreach ($results as $result) {
            $artist_images[$result->post_id] = $result->meta_value;
         }
      }
        if (!empty($additional_data)) {
            $error->add_data($additional_data, 'additional_data');
        }
      // Get regular thumbnails
      $thumbnail_ids = [];
      foreach ($items as $item) {
         if ($item->type !== BASE . 'artist' && isset($posts_by_id[$item->target_id])) {
            $thumb_id = get_post_thumbnail_id($item->target_id);
            if ($thumb_id) {
               $thumbnail_ids[$item->target_id] = $thumb_id;
            }
         }
      }
        // Log error using central error handling system
        JVB()->error()->log(
            'favourites',
            $message,
            $additional_data,
            'error'
        );
        return new WP_REST_Response([
            'success'   => false,
            'code'      => $code,
            'message'   => __($message, 'jvb'),
            'status'    => $status
        ]);
    }
      // Format each item
      foreach ($items as $item) {
         $post_id = (int)$item->target_id;
         if (!isset($posts_by_id[$post_id])) {
            continue;
         }
    /**
     * Handle queued operations for favourites
     *
     * @param WP_Error|array $result Current result
     * @param object $operation Operation from queue
     * @param array $data Current Data
     * @return array|WP_Error Processing result
     */
    public function processOperation(WP_Error|array $result, object $operation, array $data):array|WP_Error
    {
        // Check if this is a favourites-related operation type
        $favourites_operations = [
            'favourites_batch',
            'favourite_notes',
            'favourite_list_create',
            'favourite_list_update',
            'favourite_list_delete',
            'favourite_list_add',
            'favourite_list_remove',
            'favourite_list_share',
            'favourite_list_unshare'
        ];
         $post = $posts_by_id[$post_id];
        if (!in_array($operation->type, $favourites_operations)) {
            return $result; // Not our operation, pass through
        }
         $formatted_item = [
            'id' => $item->id ?? null,
            'type' => str_replace(BASE, '', $item->type),
            'target_id' => $post_id,
            'date_added' => $item->date_added ?? current_time('mysql'),
            'notes' => $item->notes ?? '',
            'url' => get_permalink($post),
            'title' => $post->post_title,
            'author' => [
               'id' => $post->post_author,
               'name' => get_the_author_meta('display_name', $post->post_author)
            ]
         ];
        try {
            $user_id = $operation->user_id;
         // Add thumbnail
         if ($item->type === BASE . 'artist') {
            $meta_value = $artist_images[$post_id] ?? null;
            $formatted_item['thumbnail'] = $meta_value ? jvbFormatImage($meta_value, 'medium', 'medium') : null;
         } else {
            $thumb_id = $thumbnail_ids[$post_id] ?? null;
            $formatted_item['thumbnail'] = $thumb_id ? jvbFormatImage($thumb_id, 'medium', 'medium') : null;
         }
            switch ($operation->type) {
                case 'favourites_batch':
                    $response = $this->processBatches($user_id, $data);
                    CacheManager::invalidateGroup($this->cache_name);
                    return $response;
         $formatted[] = $formatted_item;
      }
                case 'favourite_notes':
                    $response =  $this->processNote($user_id, $data);
                    CacheManager::invalidateGroup($this->cache_name);
                    return $response;
      return $formatted;
   }
                case 'favourite_list_create':
                    $response = $this->processListCreate($user_id, $data);
                    CacheManager::invalidateGroup('favourites_lists');
                    return $response;
   /**
    * Batch format term-type favourites to reduce queries
    */
   protected function formatTermFavourites(array $items): array
   {
      if (empty($items)) {
         return [];
      }
                case 'favourite_list_update':
                    $response = $this->processUpdateList($user_id, $data);
                    CacheManager::invalidateGroup('favourites_lists');
                    return $response;
      $formatted = [];
                case 'favourite_list_delete':
                    $response = $this->processListDeletion($user_id, $data);
                    CacheManager::invalidateGroup('favourites_lists');
                    return $response;
      // Group by taxonomy
      $terms_by_taxonomy = [];
      foreach ($items as $item) {
         $tax = $item->type;
         if (!isset($terms_by_taxonomy[$tax])) {
            $terms_by_taxonomy[$tax] = [];
         }
         $terms_by_taxonomy[$tax][] = (int)$item->target_id;
      }
                case 'favourite_list_add':
                    $response = $this->processAddToList($user_id, $data);
                    CacheManager::invalidateGroup('favourites_lists');
                    return $response;
      // Get all terms by taxonomy
      $terms_by_id = [];
      foreach ($terms_by_taxonomy as $taxonomy => $term_ids) {
         $terms = get_terms([
            'taxonomy' => $taxonomy,
            'include' => $term_ids,
            'hide_empty' => false,
         ]);
                case 'favourite_list_remove':
                    $response = $this->removeFromList($user_id, $data);
                    CacheManager::invalidateGroup('favourites_lists');
                    return $response;
         if (!is_wp_error($terms)) {
            foreach ($terms as $term) {
               $terms_by_id[$taxonomy . '_' . $term->term_id] = $term;
            }
         }
      }
                case 'favourite_list_share':
                    $response = $this->shareList($user_id, $data);
                    CacheManager::invalidateGroup('favourites_lists_shares');
                    return $response;
      // Get shop images
      $shop_ids = [];
      foreach ($items as $item) {
         if ($item->type === BASE . 'shop') {
            $shop_ids[] = (int)$item->target_id;
         }
      }
                case 'favourite_list_unshare':
                    $response = $this->unshareList($user_id, $data);
                    CacheManager::invalidateGroup('favourites_lists_shares');
                    return $response;
      $shop_images = [];
      if (!empty($shop_ids)) {
         global $wpdb;
         $placeholders = implode(',', array_fill(0, count($shop_ids), '%d'));
         $results = $wpdb->get_results($wpdb->prepare(
            "SELECT term_id, meta_value FROM {$wpdb->termmeta}
            WHERE meta_key = %s AND term_id IN ($placeholders)",
            array_merge([BASE . 'image'], $shop_ids)
         ));
                default:
                    return $result;
            }
        } catch (Exception $e) {
            JVB()->error()->log(
                '[FavouritesRoutes]:processOperation',
                'Failed to process queued operation: ' . $e->getMessage(),
                [
                    'operation_id' => $operation->id,
                    'type' => $operation->type,
                    'user_id' => $operation->user_id
                ],
                'error'
            );
         foreach ($results as $result) {
            $shop_images[$result->term_id] = $result->meta_value;
         }
      }
            return $result;
        }
    }
      // Format each item
      foreach ($items as $item) {
         $term_id = (int)$item->target_id;
         $key = $item->type . '_' . $term_id;
    /**
     * Clean up favourites when a post is deleted
     *
     * @param int $post_id The ID of the post being deleted
     */
    public function cleanupPostFavourites(int $post_id)
    {
        try {
            global $wpdb;
            $table = $wpdb->prefix . BASE . 'favourites';
            $type = get_post_type($post_id);
         if (!isset($terms_by_id[$key])) {
            continue;
         }
            if (!$type) {
                return;
            }
         $term = $terms_by_id[$key];
            $type = BASE . $type;
         $formatted_item = [
            'id' => $item->id ?? null,
            'type' => str_replace(BASE, '', $item->type),
            'target_id' => $term_id,
            'date_added' => $item->date_added ?? current_time('mysql'),
            'notes' => $item->notes ?? '',
            'title' => html_entity_decode($term->name),
            'url' => get_term_link($term)
         ];
            if (!isset($this->valid_types[$type])) {
                return;
            }
         // Add thumbnail for shops
         if ($item->type === BASE . 'shop') {
            $meta_value = $shop_images[$term_id] ?? null;
            $formatted_item['thumbnail'] = $meta_value ? jvbFormatImage($meta_value, 'medium', 'medium') : null;
         }
            // Delete favourites for this post
            $wpdb->delete(
                $table,
                [
                    'type' => $type,
                    'target_id' => $post_id
                ],
                ['%s', '%d']
            );
         $formatted[] = $formatted_item;
      }
            // Also remove from list items
            $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
            $wpdb->delete(
                $items_table,
                [
                    'item_type' => $type,
                    'item_id' => $post_id
                ],
                ['%s', '%d']
            );
      return $formatted;
   }
        } catch (Exception $e) {
            JVB()->error()->log(
                'favourites',
                'Error cleaning up favourites for deleted post: ' . $e->getMessage(),
                ['post_id' => $post_id],
                'error'
            );
        }
    }
   /**
    * Handle list operations - routes to appropriate method
    */
   public function handleList(WP_REST_Request $request): WP_REST_Response
   {
      $user_id = absint($request->get_param('user'));
      $operation_id = sanitize_text_field($request->get_param('id'));
      $action = sanitize_text_field($request->get_param('action'));
    /**
     * Clean up favourites when a term is deleted
     *
     * @param int $term_id The ID of the deleted term
     * @param int $tt_id Term taxonomy ID
     * @param string $taxonomy The taxonomy slug
     */
    public function cleanupTermFavourites(int $term_id, int $tt_id, string $taxonomy)
    {
        try {
            if (!isset($this->valid_types[$taxonomy])) {
                return;
            }
      if (!$this->userCheck($user_id)) {
         return $this->unauthorized();
      }
            global $wpdb;
            $table = $wpdb->prefix . BASE . 'favourites';
      $data = [
         'action' => $action,
         'list_id' => absint($request->get_param('list_id') ?? 0),
         'name' => sanitize_text_field($request->get_param('name') ?? ''),
         'description' => sanitize_textarea_field($request->get_param('description') ?? ''),
         'items' => $request->get_param('items') ?? [],
         'email' => sanitize_email($request->get_param('email') ?? ''),
         'permission_type' => sanitize_text_field($request->get_param('permission_type') ?? 'view'),
      ];
            // Delete favourites for this term
            $wpdb->delete(
                $table,
                [
                    'type' => $taxonomy,
                    'target_id' => $term_id
                ],
                ['%s', '%d']
            );
      // Map action to operation type
      $operation_type = match ($action) {
         'create' => 'favourite_list_create',
         'update' => 'favourite_list_update',
         'delete' => 'favourite_list_delete',
         'add_items' => 'favourite_list_add_items',
         'remove_items' => 'favourite_list_remove_items',
         'share' => 'favourite_list_share',
         'unshare' => 'favourite_list_unshare',
         default => null
      };
            // Also remove from list items
            $items_table = $wpdb->prefix . BASE . 'favourites_list_items';
            $wpdb->delete(
                $items_table,
                [
                    'item_type' => $taxonomy,
                    'item_id' => $term_id
                ],
                ['%s', '%d']
            );
      if (!$operation_type) {
         return $this->error('Invalid action', 'invalid_action', 400);
      }
            // Clean up list stats
            $stats_table = $wpdb->prefix . BASE . 'favourites_list_stats';
            $wpdb->delete(
                $stats_table,
                [
                    'item_type' => $taxonomy,
                    'item_id' => $term_id
                ],
                ['%s', '%d']
            );
      JVB()->queue()->queueOperation(
         $operation_type,
         $user_id,
         $data,
         [
            'operation_id' => $operation_id,
            'priority' => 'normal',
         ]
      );
        } catch (Exception $e) {
            JVB()->error()->log(
                'favourites',
                'Error cleaning up favourites for deleted term: ' . $e->getMessage(),
                ['term_id' => $term_id, 'taxonomy' => $taxonomy],
                'error'
            );
        }
    }
      return $this->queued($operation_id);
   }
}