From 86c6cd3cc099d2480932ede03c12cea01e625c94 Mon Sep 17 00:00:00 2001
From: Jake Vanderwerf <get@jakevanderwerf.ca>
Date: Sun, 26 Apr 2026 21:56:28 +0000
Subject: [PATCH] =Requiring files based on Site class settings

---
 inc/rest/routes/FavouritesRoutes.php | 5112 +++++++++++++++++++++-------------------------------------
 1 files changed, 1,878 insertions(+), 3,234 deletions(-)

diff --git a/inc/rest/routes/FavouritesRoutes.php b/inc/rest/routes/FavouritesRoutes.php
index b1483b3..3494f5b 100644
--- a/inc/rest/routes/FavouritesRoutes.php
+++ b/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 ((bool)$result) {
+			$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 ((bool) $result) $results['added']++;
+					} else {
+						$deleted = $table->delete([
+							'user_id' => $user_id,
+							'type' => $type,
+							'target_id' => $target_id
+						]);
+						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);
+	}
 }

--
Gitblit v1.10.0