Jake Vanderwerf
5 days ago a9b3b28d001941921aa70d37fdc87c758a163a44
inc/rest/routes/ResponseRoutes.php
@@ -2,718 +2,510 @@
namespace JVBase\rest\routes;
use JVBase\JVB;
use JVBase\rest\RestRouteManager;
use JVBase\managers\CacheManager;
use JVBase\rest\Rest;
use JVBase\managers\CustomTable;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
if (!defined('ABSPATH')) {
    exit; // Exit if accessed directly
   exit;
}
class ResponseRoutes extends RestRouteManager
/**
 * Response Routes
 *
 * Handles threaded responses/comments for news items
 */
class ResponseRoutes extends Rest
{
    protected int $per_page;
    protected false|object $manager = false;
   protected int $perPage = 20;
   protected CustomTable $table;
   protected CustomTable $karmaTable;
    public function __construct()
    {
        $this->cache_name = 'responses';
        parent::__construct();
        $this->action = 'dash-';
        $this->per_page = 20;
   public function __construct()
   {
      $this->cacheName = 'responses';
      $this->cacheTtl = 1800; // 30 minutes
      parent::__construct();
        add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
        add_action('deleted_user', [$this, 'handleUserDeletion'], 10, 1);
    }
      $this->table = CustomTable::for('responses');
      $this->karmaTable = CustomTable::for('karma_response');
    /**
     * Registers response routes
     * @return void
     */
    public function registerRoutes():void
    {
        register_rest_route($this->namespace, '/response', [
            [
                'methods'   => 'GET',
                'callback'  => [$this, 'getResponses'],
                'permission_callback'   => [$this, 'checkPermission']
            ],
            [
                'methods'   => 'POST',
                'callback'  => [$this, 'handleResponseActions'],
                'permission_callback'   => [$this, 'checkPermission']
            ]
        ]);
    }
      add_filter(BASE.'handle_bulk_operation', [$this, 'processOperation'], 10, 3);
      add_action('deleted_user', [$this, 'handleUserDeletion'], 10, 1);
   }
    /**
     * Get responses for a news post
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function getResponses(WP_REST_Request $request):WP_REST_Response
    {
        $item_id = (int) $request->get_param('item_id');
        error_log('Item ID: '.print_r($item_id, true));
        if (!$item_id) {
            return new WP_REST_Response([
                'success'=> false,
                'message'   => 'Missing item ID'
            ]);
        }
   public function registerRoutes(): void
   {
      register_rest_route($this->namespace, '/response', [
         [
            'methods'   => 'GET',
            'callback'  => [$this, 'getResponses'],
            'permission_callback' => 'is_user_logged_in'
         ],
         [
            'methods'   => 'POST',
            'callback'  => [$this, 'handleResponseActions'],
            'permission_callback' => 'is_user_logged_in'
         ]
      ]);
   }
        // Build query args
        $args = $this->buildQueryArgs($request);
        error_log('Args: '.print_r($args, true));
        $responses = $this->getItemResponse($item_id, $args);
        error_log('Responses: '.print_r($responses, true));
   /**
    * Get responses for an item
    */
   public function getResponses(WP_REST_Request $request): WP_REST_Response
   {
      $item_id = (int) $request->get_param('item_id');
        // Return formatted response
        return new WP_REST_Response($responses);
    }
      if (!$item_id) {
         return $this->error('Missing item ID', 'missing_item_id');
      }
    /**
     * @param int $ID
     * @param string $postType
     *
     * @return void
     */
    protected function clearItemCache(int $ID, string $postType):void
    {
        $args = [
      // Build query args
      $args = $this->buildQueryArgs($request);
      $cacheKey = $this->cache->generateKey(array_merge(['item_id' => $item_id], $args));
            BASE.'news' => $ID,
            'post_type' => BASE.$postType,
            'page'      => 1,
            'per_page'  => 20,
            'orderby'   => 'created_at',
            'order'     => 'DESC'
        ];
        $key = $this->cache->generateKey($args);
        $this->cache->invalidate($key);
    }
      // Check headers for 304 Not Modified
      $headerCheck = $this->checkHeaders($request, $cacheKey);
      if ($headerCheck) {
         return $headerCheck;
      }
    /**
     * @param int $ID
     * @param array $args
     *
     * @return array|WP_Error
     */
    public function getItemResponse(int $ID, array $args = []):array|WP_Error
    {
      // Check cache
      $cached = $this->cache->get($cacheKey);
      if ($cached) {
         return $this->success($cached);
      }
        $default = [
            'post_type' => BASE.'news',
            'page' => 1,
            'per_page' => 20,
            'orderby' => 'created_at',
            'order' => 'DESC'
        ];
      // Get responses
      $responses = $this->getItemResponse($item_id, $args);
        $args = wp_parse_args($default, $args);
      if (is_wp_error($responses)) {
         return $this->notFound($responses->get_error_message());
      }
        $key = $this->cache->generateKey(array_merge([$args['post_type'] => $ID], $args));
        $check = $this->cache->get($key);
      // Cache and return
      $this->cache->set($cacheKey, $responses);
      return $this->success($responses);
   }
        if ($check) {
            return $check;
        }
   /**
    * Handle response actions (create/update/delete)
    */
   public function handleResponseActions(WP_REST_Request $request): WP_REST_Response
   {
      $data = $request->get_params();
      $user_id = (int) ($data['user'] ?? 0);
        // Verify post exists and is of correct type
        $post = get_post($ID);
        if (!$post || $post->post_type !== $args['post_type']) {
            return new WP_Error(
                self::ERROR_NOT_FOUND,
                'Item not found',
                ['status' => 404]
            );
        }
      if (!$this->userCheck($user_id)) {
         return $this->unauthorized('User verification failed');
      }
        // Execute the query
        global $wpdb;
        $table = $wpdb->prefix . BASE . 'responses';
      $operation_id = $data['id'] ?? uniqid('response_');
      $action = sanitize_text_field($data['action'] ?? '');
        // Get total count
        $total_query = "SELECT COUNT(*) FROM $table WHERE item_id = %d";
        $total_args = [$ID];
      if (!in_array($action, ['create', 'delete', 'update'])) {
         return $this->error('Invalid action', 'invalid_action');
      }
        // Add parent filter if specified
        if (isset($args['parent_id'])) {
            $total_query .= " AND parent_id " . ($args['parent_id'] === null ? "IS NULL" : "= %d");
            if ($args['parent_id'] !== null) {
                $total_args[] = $args['parent_id'];
            }
        }
      // Validate required fields for create
      if ($action === 'create') {
         if (!isset($data['item_id'], $data['response'], $data['content'])) {
            return $this->error('Missing required fields', 'missing_fields');
         }
      }
        $total_items = $wpdb->get_var($wpdb->prepare($total_query, $total_args));
      // Prepare data for queue
      $queue_data = match($action) {
         'create' => [
            'item_id' => (int) $data['item_id'],
            'parent_id' => isset($data['parent_id']) ? (int) $data['parent_id'] : null,
            'response' => wp_kses_post($data['response']),
            'content' => sanitize_text_field($data['content'])
         ],
         'update' => [
            'response_id' => (int) $data['response_id'],
            'response' => isset($data['response']) ? wp_kses_post($data['response']) : null,
            'status' => isset($data['status']) && in_array($data['status'], ['published', 'hidden', 'flagged'])
               ? $data['status']
               : null
         ],
         'delete' => [
            'response_id' => (int) $data['response_id']
         ]
      };
        // Build main query
        $query = "SELECT r.*,
                  COALESCE((SELECT COUNT(*) FROM $table WHERE parent_id = r.id), 0) as reply_count,
                  COALESCE((SELECT SUM(CASE WHEN vote = 'up' THEN 1 ELSE 0 END) FROM {$wpdb->prefix}" . BASE . "karma_response WHERE item_id = r.id), 0) as upvotes,
                  COALESCE((SELECT SUM(CASE WHEN vote = 'down' THEN 1 ELSE 0 END) FROM {$wpdb->prefix}" . BASE . "karma_response WHERE item_id = r.id), 0) as downvotes
                  FROM $table r
                  WHERE r.item_id = %d";
      // Queue the operation
      JVB()->queue()->queueOperation(
         $action . '_response',
         $user_id,
         $queue_data,
         [
            'operation_id' => 'u' . $user_id . '_' . $operation_id,
            'priority' => 'high'
         ]
      );
        $query_args = [$ID];
      return $this->queued($operation_id);
   }
        // Apply parent filter
        if (isset($args['parent_id'])) {
            $query .= " AND r.parent_id " . ($args['parent_id'] === null ? "IS NULL" : "= %d");
            if ($args['parent_id'] !== null) {
                $query_args[] = $args['parent_id'];
            }
        }
   /**
    * Get responses with karma calculations
    */
   protected function getItemResponse(int $item_id, array $args = []): array|WP_Error
   {
      $defaults = [
         'page' => 1,
         'per_page' => $this->perPage,
         'orderby' => 'created_at',
         'order' => 'DESC',
         'parent_id' => null
      ];
        // Apply order
        $order_column = in_array($args['orderby'], ['created_at', 'updated_at'])
            ? "r." . $args['orderby']
            : $args['orderby'];
      $args = wp_parse_args($args, $defaults);
        $query .= " ORDER BY $order_column " . $args['order'];
      // Verify post exists
      $post = get_post($item_id);
      if (!$post) {
         return new WP_Error('not_found', 'Item not found');
      }
        // Apply pagination
        $query .= " LIMIT %d OFFSET %d";
        $query_args[] = $args['per_page'];
        $query_args[] = ($args['page'] - 1) * $args['per_page'];
      // Build query with karma calculations
      $query = "
            SELECT r.*,
                COALESCE((SELECT COUNT(*) FROM {$this->table->getFullTableName()} WHERE parent_id = r.id), 0) as reply_count,
                COALESCE((SELECT COUNT(*) FROM {$this->karmaTable->getFullTableName()} WHERE item_id = r.id AND vote = 'up'), 0) as upvotes,
                COALESCE((SELECT COUNT(*) FROM {$this->karmaTable->getFullTableName()} WHERE item_id = r.id AND vote = 'down'), 0) as downvotes
            FROM {$this->table->getFullTableName()} r
            WHERE r.item_id = %d
        ";
        // Get responses
        $responses = $wpdb->get_results($wpdb->prepare($query, $query_args));
      $query_args = [$item_id];
        // Format responses
        $items = array_map([$this, 'formatItem'], $responses);
      // Filter by parent_id
      if (isset($args['parent_id'])) {
         $query .= " AND r.parent_id " . ($args['parent_id'] === null ? "IS NULL" : "= %d");
         if ($args['parent_id'] !== null) {
            $query_args[] = $args['parent_id'];
         }
      }
        // Calculate pagination
        $total_pages = ceil($total_items / $args['per_page']);
      // Get total count for pagination
      $count_query = str_replace(
         ['SELECT r.*,', 'COALESCE((SELECT COUNT(*) FROM', 'as reply_count,', 'as upvotes,', 'as downvotes'],
         ['SELECT COUNT(*)', '', '', '', ''],
         $query
      );
      $count_query = preg_replace('/COALESCE\([^)]+\)[^,]*,?/', '', $count_query);
      $total_items = (int) $this->table->queryVar($count_query, $query_args);
        $return =  [
            'items' => $items,
            'has_more' => $args['page'] < $total_pages,
            'total_items' => (int) $total_items,
            'total_pages' => $total_pages
        ];
      // Add ordering
      $order_column = in_array($args['orderby'], ['created_at', 'updated_at'])
         ? "r.{$args['orderby']}"
         : $args['orderby'];
        $this->cache->set($key, $return);
        return $return;
    }
      $query .= " ORDER BY {$order_column} {$args['order']}";
    /**
     * Create a new response
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function handleResponseActions(WP_REST_Request $request):WP_REST_Response
    {
        $data = $request->get_params();
        $user_id = (int)$data['user'];
        if (!$this->userCheck($data['user'])) {
            return new WP_REST_Response([
                'success'   => false,
                'message'   => 'User doesn\'t match. Bot?'
            ]);
        }
        $operation_id = $data['id'] ?? uniqid('response_');
      // Add pagination
      $query .= " LIMIT %d OFFSET %d";
      $query_args[] = $args['per_page'];
      $query_args[] = ($args['page'] - 1) * $args['per_page'];
      // Get responses
      $responses = $this->table->queryResults($query, $query_args);
        // Validate required fields
        if (!array_key_exists('item_id', $data) || !array_key_exists('response', $data) || !array_key_exists('content', $data)) {
            error_log('Not enough data');
            return new WP_REST_Response([
                'success'   => false,
                'message'   => 'Missing required information.'
            ]);
        }
      // Format responses
      $items = array_map([$this, 'formatItem'], $responses);
        // Prepare data for queue
        $queue_data = [
            'item_id' => (int) $data['item_id'],
            'parent_id' => array_key_exists('parent_id', $data) ? (int) $data['parent_id'] : null,
            'response' => wp_kses_post($data['response']),
            'content'=> sanitize_text_field($data['content'])
        ];
      return [
         'items' => $items,
         'has_more' => $args['page'] < ceil($total_items / $args['per_page']),
         'total_items' => $total_items,
         'total_pages' => (int) ceil($total_items / $args['per_page'])
      ];
   }
        error_log('Queue Data: '.print_r($queue_data, true));
   /**
    * Process queue operations
    */
   public function processOperation(WP_Error|array $result, object $operation, array $data): WP_Error|array
   {
      if (!in_array($operation->type, ['create_response', 'update_response', 'delete_response'])) {
         return $result;
      }
        $action = sanitize_text_field($data['action']);
        error_log('Action: '.print_r($action, true));
        if (!in_array($action, ['create', 'delete', 'update'])) {
            return new WP_REST_Response([
                'success'   => false,
                'message'   => 'Invalid action'
            ]);
        }
        error_log('Sanitized action. Here we go!');
      return match($operation->type) {
         'create_response' => $this->createResponse($operation, $data),
         'update_response' => $this->updateResponse($data),
         'delete_response' => $this->deleteResponse($data),
         default => $result
      };
   }
        $this->queue = JVB()->queue();
        // Add to queue
        $operation = $this->queue->queueOperation(
            $action.'_response',
            $user_id,
            $queue_data,
            [
                'operation_id' => 'u' . $user_id . '_' . $operation_id,
                'priority'     => 'high'
            ]
        );
        error_log('Queued for processing');
   /**
    * Create a new response
    */
   protected function createResponse(object $operation, array $data): array
   {
      $response_id = $this->table->insert([
         'item_id' => $data['item_id'],
         'content' => $data['content'],
         'user_id' => $operation->user_id,
         'parent_id' => $data['parent_id'],
         'response' => $data['response'],
         'status' => 'published'
      ]);
        return new WP_REST_Response([
            'success'   => true,
            'message'   => 'Item queued for processing'
        ]);
    }
      if (!$response_id) {
         $this->logError('Failed to insert response', [
            'data' => $data,
            'error' => $this->table->getLastError()
         ]);
    /**
     * Update a response
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function updateResponse(WP_REST_Request $request):WP_REST_Response
    {
        $this->queue = JVB()->queue();
        $id = (int) $request->get_param('id');
        $data = $request->get_params();
        $user_id = (int) $data['user'] ?? get_current_user_id();
        $operation_id = $data['id'] ?? uniqid('response_update_');
         return ['success' => false, 'result' => 'Failed to create response'];
      }
      // Send notifications
      $this->sendNotifications($data['item_id'], $data['parent_id'], $response_id, $operation->user_id);
        // Verify response exists
        global $wpdb;
        $table = $wpdb->prefix . BASE . 'responses';
        $response = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id));
      // Clear cache
      $this->cache->flush();
        if (!$response) {
            return new WP_REST_Response([
                'success'   => false,
                'message'   => 'Item not found.'
            ]);
        }
      return ['success' => true, 'result' => $response_id];
   }
        // Check ownership or admin rights
        if ($response->user_id != $user_id) {
            return new WP_REST_Response([
                'success'   => false,
                'message'   => 'You do not have permission to delete this response'
            ]);
        }
   /**
    * Update a response
    */
   protected function updateResponse(array $data): array
   {
      $update_data = [];
        // Prepare data for queue
        $queue_data = [
            'response_id' => $id,
            'response' => !empty($data['response']) ? wp_kses_post($data['response']) : null,
            'status' => !empty($data['status']) && in_array($data['status'], ['published', 'hidden', 'flagged'])
                ? $data['status']
                : null
        ];
      if (isset($data['response'])) {
         $update_data['response'] = $data['response'];
         $update_data['updated_at'] = current_time('mysql');
      }
        // Add to queue
        $operation = $this->queue->queueOperation(
            'update_response',
            $user_id,
            $queue_data,
            [
                'operation_id' => 'u' . $user_id . '_' . $operation_id,
                'priority'     => 'high',
                'notification' => false,
            ]
        );
      if (isset($data['status'])) {
         $update_data['status'] = $data['status'];
      }
        return new WP_REST_Response($operation);
    }
      if (empty($update_data)) {
         return ['success' => false, 'result' => 'No fields to update'];
      }
    /**
     * Delete a response
     * @param WP_REST_Request $request
     * @return WP_REST_Response
     */
    public function deleteResponse(WP_REST_Request $request):WP_REST_Response
    {
        $this->queue = JVB()->queue();
        $id = (int) $request->get_param('id');
        $user_id = get_current_user_id();
        $operation_id = $request->get_param('id') ?? uniqid('response_delete_');
      $updated = $this->table->update(
         $update_data,
         ['id' => $data['response_id']]
      );
        // Verify response exists
        global $wpdb;
        $table = $wpdb->prefix . BASE . 'responses';
        $response = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $id));
      if ($updated === false) {
         $this->logError('Failed to update response', [
            'data' => $data,
            'error' => $this->table->getLastError()
         ]);
        if (!$response) {
            return new WP_REST_Response([
                'success'       => false,
                'message'       => 'Response not found',
            ]);
        }
        // Check ownership or admin rights
        if ($response->user_id != $user_id) {
            return new WP_REST_Response([
                'success'   => false,
                'msesage'   => 'You do not have permission to delete this resposne',
            ]);
        }
        // Add to queue
        $operation = $this->queue->queueOperation(
            'delete_response',
            $user_id,
            ['response_id' => $id],
            [
                'operation_id' => 'u' . $user_id . '_' . $operation_id,
                'priority'     => 'high',
                'notification' => false,
            ]
        );
        return new WP_REST_Response([
            'success'   => true,
            'message'   => 'Queued for processing'
        ]);
    }
    /**
     * Process operations from the queue
     * @param WP_Error|array $result
     * @param object $operation
     * @param array $data
     * @return WP_Error|array
     */
    public function processOperation(WP_Error|array $result, object $operation, array $data):WP_Error|array
    {
        if (!in_array($operation->type, ['create_response', 'update_response', 'delete_response'])) {
            return $result;
        }
        global $wpdb;
        $table = $wpdb->prefix . BASE . 'responses';
        switch ($operation->type) {
            case 'create_response':
                // Create new response
                $inserted = $wpdb->insert(
                    $table,
                    [
                        'item_id'   => $data['item_id'],
                        'content' => $data['content'],
                        'user_id'   => $operation->user_id,
                        'parent_id' => $data['parent_id'],
                        'response'   => $data['response'],
                        'status'    => 'published',
                        'created_at' => current_time('mysql'),
                        'updated_at' => current_time('mysql')
                    ],
                    ['%d', '%d', '%d', '%s', '%s', '%s', '%s']
                );
                if (!$inserted) {
                    error_log('Did not insert'.print_r($wpdb->last_error, true));
                    JVB()->error()->log(
                        '[ResponseRoutes]:processOperation',
                        'Failed to insert response',
                        ['data' => $data, 'error' => $wpdb->last_error],
                        'error'
                    );
                    return [
                  'success'   => false,
                  'result' => 'Failed to create response'
               ];
                }
                $response_id = $wpdb->insert_id;
                error_log('Response ID: '.print_r($response_id, true));
                // Send notification to post author
                $post = get_post($data['item_id']);
                if ($post && $post->post_author != $operation->user_id) {
                    JVB()->notification()->addNotification(
                        $post->post_author,
                        'new_response',
                        [
                            'message' => 'Someone responded to your post',
                            'item_id' => $data['item_id'],
                            'response_id' => $response_id
                        ]
                    );
                }
                // Send notification to parent response author if this is a reply
                if ($data['parent_id']) {
                    $parent = $wpdb->get_row($wpdb->prepare(
                        "SELECT user_id FROM $table WHERE id = %d",
                        $data['parent_id']
                    ));
                    if ($parent && $parent->user_id != $operation->user_id) {
                        JVB()->notification()->addNotification(
                            $parent->user_id,
                            'response_reply',
                            [
                                'message' => 'Someone replied to your response',
                                'item_id' => $data['item_id'],
                                'response_id' => $response_id
                            ]
                        );
                    }
                }
                $this->clearItemCache($data['item_id'], $data['content']);
                CacheManager::invalidateGroup($this->cache_name);
                return ['success' => true, 'result' => $response_id];
            case 'update_response':
                $update_data = [];
                $update_format = [];
                if (array_key_exists('response', $data)) {
                    $update_data['response'] = $data['response'];
                    $update_data['updated_at'] = current_time('mysql');
                    $update_format[] = '%s';
                    $update_format[] = '%s';
                }
                if (array_key_exists('status', $data)) {
                    $update_data['status'] = $data['status'];
                    $update_format[] = '%s';
                }
                if (empty($update_data)) {
                    return ['success' => false, 'result' => 'No fields to update'];
                }
         return ['success' => false, 'result' => 'Failed to update response'];
      }
                $updated = $wpdb->update(
                    $table,
                    $update_data,
                    ['id' => $data['response_id']],
                    $update_format,
                    ['%d']
                );
      $this->cache->flush();
      return ['success' => true, 'result' => $updated];
   }
                if ($updated === false) {
                    JVB()->error()->log(
                        '[ResponseRoutes]:processOperation',
                        'Failed to update response',
                        ['data' => $data, 'error' => $wpdb->last_error],
                        'error'
                    );
                    return [
                  'success'   => false,
                  'result' => 'Failed to update response'
               ];
                }
   /**
    * Delete a response (or mark as deleted if it has replies)
    */
   protected function deleteResponse(array $data): array
   {
      $response = $this->table->get(['id' => $data['response_id']]);
                $this->clearItemCache($data['item_id'], $data['content']);
                CacheManager::invalidateGroup($this->cache_name);
                return ['success' => true, 'result' => $updated];
      if (!$response) {
         return ['success' => false, 'result' => 'Response not found'];
      }
            case 'delete_response':
                // Get response info before deleting
                $response = $wpdb->get_row($wpdb->prepare(
                    "SELECT * FROM $table WHERE id = %d",
                    $data['response_id']
                ));
      // Check if it has replies
      $has_replies = $this->table->where(['parent_id' => $data['response_id']])->countResults() > 0;
                if (!$response) {
                    return ['success' => false, 'result' => 'Response not found'];
                }
      if ($has_replies) {
         // Don't delete, just mark as deleted
         $updated = $this->table->update(
            [
               'response' => '[ deleted ]',
               'status' => 'deleted',
               'updated_at' => current_time('mysql')
            ],
            ['id' => $data['response_id']]
         );
                // Check if this response has replies
                $has_replies = $wpdb->get_var($wpdb->prepare(
                    "SELECT COUNT(*) FROM $table WHERE parent_id = %d",
                    $data['response_id']
                )) > 0;
         $this->cache->flush();
         return ['success' => true, 'result' => $updated];
      }
                if ($has_replies) {
                    // Don't delete, just mark as deleted and replace content
                    $updated = $wpdb->update(
                        $table,
                        [
                            'response' => '[ deleted ]',
                            'status' => 'deleted',
                            'updated_at' => current_time('mysql')
                        ],
                        ['id' => $data['response_id']],
                        ['%s', '%s', '%s'],
                        ['%d']
                    );
                    CacheManager::invalidateGroup($this->cache_name);
                    return ['success' => true, 'result' => $updated ];
                } else {
                    // No replies, safe to actually delete
                    $deleted = $wpdb->delete(
                        $table,
                        ['id' => $data['response_id']],
                        ['%d']
                    );
      // No replies, safe to delete
      $deleted = $this->table->delete(['id' => $data['response_id']]);
                    if ($deleted === false) {
                        JVB()->error()->log(
                            '[ResponseRoutes]:processOperation',
                            'Failed to delete response',
                            ['data' => $data, 'error' => $wpdb->last_error],
                            'error'
                        );
                        return [
                     'success'   => false,
                     'result' => 'Failed to delete response'
                  ];
                    }
      if ($deleted === false) {
         $this->logError('Failed to delete response', [
            'data' => $data,
            'error' => $this->table->getLastError()
         ]);
                    $this->clearItemCache($data['item_id'], $data['content']);
                    CacheManager::invalidateGroup($this->cache_name);
                    return ['success' => true, 'result' => $deleted];
                }
        }
         return ['success' => false, 'result' => 'Failed to delete response'];
      }
      $this->cache->flush();
      return ['success' => true, 'result' => $deleted];
   }
        return $result;
    }
   /**
    * Send notifications for new responses
    */
   protected function sendNotifications(int $item_id, ?int $parent_id, int $response_id, int $user_id): void
   {
      // Notify post author
      $post = get_post($item_id);
      if ($post && $post->post_author != $user_id) {
         JVB()->notification()->addNotification(
            $post->post_author,
            'new_response',
            [
               'message' => 'Someone responded to your post',
               'item_id' => $item_id,
               'response_id' => $response_id
            ]
         );
      }
    /**
     * Build query arguments from request parameters
     * @param WP_REST_Request $request
     * @return array
     */
    protected function buildQueryArgs(WP_REST_Request $request):array
    {
        $page = max(1, (int) $request->get_param('page') ?? 1);
        $per_page = min(100, max(1, (int) $request->get_param('per_page') ?? $this->per_page));
      // Notify parent response author if this is a reply
      if ($parent_id) {
         $parent = $this->table->get(['id' => $parent_id]);
        $args = [
            'page' => $page,
            'per_page' => $per_page,
            'orderby' => 'created_at',
            'order' => 'DESC'
        ];
         if ($parent && $parent->user_id != $user_id) {
            JVB()->notification()->addNotification(
               $parent->user_id,
               'response_reply',
               [
                  'message' => 'Someone replied to your response',
                  'item_id' => $item_id,
                  'response_id' => $response_id
               ]
            );
         }
      }
   }
        // Apply parent filter (null means top-level responses)
        if ($request->has_param('parent_id')) {
            $parent_id = $request->get_param('parent_id');
            $args['parent_id'] = $parent_id === '0' ? null : (int) $parent_id;
        }
   /**
    * Build query arguments from request
    */
   protected function buildQueryArgs(WP_REST_Request $request): array
   {
      $page = max(1, (int) ($request->get_param('page') ?? 1));
      $per_page = min(100, max(1, (int) ($request->get_param('per_page') ?? $this->perPage)));
        // Apply ordering
        if ($request->has_param('orderby')) {
            $orderby = $request->get_param('orderby');
            $valid_orderby = ['created_at', 'updated_at', 'upvotes', 'downvotes'];
      $args = [
         'page' => $page,
         'per_page' => $per_page,
         'orderby' => 'created_at',
         'order' => 'DESC'
      ];
            if (in_array($orderby, $valid_orderby)) {
                $args['orderby'] = $orderby;
            }
        }
      // Parent filter (null = top-level only)
      if ($request->has_param('parent_id')) {
         $parent_id = $request->get_param('parent_id');
         $args['parent_id'] = $parent_id === '0' ? null : (int) $parent_id;
      }
        if ($request->has_param('order')) {
            $order = strtoupper($request->get_param('order'));
            if (in_array($order, ['ASC', 'DESC'])) {
                $args['order'] = $order;
            }
        }
      // Ordering
      if ($request->has_param('orderby')) {
         $orderby = $request->get_param('orderby');
         $valid = ['created_at', 'updated_at', 'upvotes', 'downvotes'];
        return $args;
    }
         if (in_array($orderby, $valid)) {
            $args['orderby'] = $orderby;
         }
      }
    /**
     * @param int $itemID
     * @param int $ID
     *
     * @return array
     */
    protected function getChildren(int $itemID, int $ID):array
    {
        return $this->getItemResponse($itemID, ['parent_id' => $ID]);
    }
    /**
     * Format a response object for API output
     * @param object $response
     * @return array
     */
    protected function formatItem(object $response):array
    {
        if ($response->is_user_deleted) {
            // For deleted users, show anonymous info
            $formatted = [
                'id' => (int) $response->id,
                'item_id' => (int) $response->item_id,
                'parent_id' => $response->parent_id ? (int) $response->parent_id : null,
                'response' => $response->response,
                'status' => $response->status,
                'created_at' => $response->created_at,
                'updated_at' => $response->updated_at,
                'children'  => $this->getChildren($response->item_id, $response->id),
                'user' => [
                    'id' => null,
                    'name' => '[deleted user]',
                    'is_deleted' => true
                ],
                'upvotes' => (int) ($response->upvotes ?? 0),
                'downvotes' => (int) ($response->downvotes ?? 0),
                'karma' => (int) ($response->upvotes ?? 0) - (int) ($response->downvotes ?? 0)
            ];
        } else {
            // Normal user processing as before
            error_log('Response: '.print_r($response, true));
            $artist = jvbContentFromUser($response->user_id);
      if ($request->has_param('order')) {
         $order = strtoupper($request->get_param('order'));
         if (in_array($order, ['ASC', 'DESC'])) {
            $args['order'] = $order;
         }
      }
            $formatted = [
                'id' => (int) $response->id,
                'item_id' => (int) $response->item_id,
                'parent_id' => $response->parent_id ? (int) $response->parent_id : null,
                'response' => $response->response,
                'status' => $response->status,
                'created_at' => $response->created_at,
                'updated_at' => $response->updated_at,
                'children'  => $this->getChildren($response->item_id, $response->id),
                'upvotes' => (int) ($response->upvotes ?? 0),
                'downvotes' => (int) ($response->downvotes ?? 0),
                'karma' => (int) ($response->upvotes ?? 0) - (int) ($response->downvotes ?? 0)
      return $args;
   }
            ];
   /**
    * Get child responses recursively
    */
   protected function getChildren(int $item_id, int $parent_id): array
   {
      return $this->getItemResponse($item_id, ['parent_id' => $parent_id]);
   }
            // Add artist info if available
            if (!empty($artist)) {
                $formatted['artist'] = [
                    'name' => $artist['name'],
                    'url' => $artist['url'],
                    'shop' => $artist['shop'] ?? null
                ];
            }
        }
   /**
    * Format response for API output
    */
   protected function formatItem(object $response): array
   {
      $formatted = [
         'id' => (int) $response->id,
         'item_id' => (int) $response->item_id,
         'parent_id' => $response->parent_id ? (int) $response->parent_id : null,
         'response' => $response->response,
         'status' => $response->status,
         'created_at' => $response->created_at,
         'updated_at' => $response->updated_at,
         'children' => $this->getChildren($response->item_id, $response->id),
         'upvotes' => (int) ($response->upvotes ?? 0),
         'downvotes' => (int) ($response->downvotes ?? 0),
         'karma' => (int) ($response->upvotes ?? 0) - (int) ($response->downvotes ?? 0),
         'reply_count' => (int) ($response->reply_count ?? 0)
      ];
        // Add reply count if available (for both deleted and non-deleted users)
        if (isset($response->reply_count)) {
            $formatted['reply_count'] = (int) $response->reply_count;
        }
      // Handle deleted users
      if ($response->is_user_deleted) {
         $formatted['user'] = [
            'id' => null,
            'name' => '[deleted user]',
            'is_deleted' => true
         ];
      } else {
         // Add artist info
         $artist = jvbContentFromUser($response->user_id);
         if (!empty($artist)) {
            $formatted['artist'] = [
               'name' => $artist['name'],
               'url' => $artist['url'],
               'shop' => $artist['shop'] ?? null
            ];
         }
      }
        return $formatted;
    }
      return $formatted;
   }
    /**
     * Handle user deletion by anonymizing their responses
     * @param int $user_id The deleted user ID
     */
    public function handleUserDeletion(int $user_id):void
    {
        global $wpdb;
        $table = $wpdb->prefix . BASE . 'news_responses';
   /**
    * Handle user deletion by anonymizing responses
    */
   public function handleUserDeletion(int $user_id): void
   {
      $updated = $this->table->update(
         [
            'is_user_deleted' => 1,
            'updated_at' => current_time('mysql')
         ],
         ['user_id' => $user_id]
      );
        // Anonymize all responses by this user
        $wpdb->update(
            $table,
            [
                'is_user_deleted' => 1,
                // Keep user_id intact for internal tracking, but mark as deleted
                'updated_at' => current_time('mysql')
            ],
            ['user_id' => $user_id],
            ['%d', '%s'],
            ['%d']
        );
      $this->logError(
         'Anonymized responses for deleted user',
         ['user_id' => $user_id, 'count' => $updated],
         'info'
      );
        JVB()->error()->log(
            'news_responses',
            'Anonymized responses for deleted user',
            ['user_id' => $user_id, 'count' => $wpdb->rows_affected],
            'info'
        );
    }
      $this->cache->flush();
   }
}