<?php
/**
 * ReviewManager — all business logic for Module 11: Product Reviews.
 *
 * Tables used:
 *   tc_reviews      — review records (rating, body, status, helpful/not_helpful counters)
 *   tc_review_votes — one vote per (review × user/ip) to prevent double-voting
 *
 * Settings (section = 'reviews' in tc_settings):
 *   reviews_enabled           1 = on (default)
 *   reviews_auto_approve      0 = pending queue (default)
 *   reviews_require_purchase  0 = anyone can review (default)
 *   reviews_allow_guest       1 = guests allowed (default)
 *
 * @package TitanCart\Modules\Reviews
 */

namespace TitanCart\Modules\Reviews;

defined( 'ABSPATH' ) || exit;

class ReviewManager {

    // ══════════════════════════════════════════════════════════════════════════
    // SETTINGS HELPER
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Read a single reviews setting from tc_settings.
     *
     * Falls back to $default when the row is absent (handles fresh installs
     * before settings are seeded).
     *
     * @param  string $key
     * @param  mixed  $default
     * @return mixed
     */
    private function setting( string $key, mixed $default = null ): mixed {
        $db  = titancart_db();
        $val = $db->getField(
            "SELECT value FROM {$db->table('settings')} WHERE section = 'reviews' AND `key` = ?s LIMIT 1",
            $key
        );
        return $val !== null ? $val : $default;
    }

    // ══════════════════════════════════════════════════════════════════════════
    // STOREFRONT — READ
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Return a paginated list of APPROVED reviews for a product.
     *
     * Each row includes author name (from tc_customers join), rating, title,
     * body, helpful/not_helpful counts, verified purchase flag, and age.
     *
     * @param  int    $product_id
     * @param  int    $page
     * @param  int    $per_page
     * @param  string $sort  newest|oldest|highest|lowest|helpful
     * @return array{ reviews: array, total: int, pages: int }
     */
    public function get_reviews(
        int $product_id,
        int $page     = 1,
        int $per_page = 10,
        string $sort  = 'newest'
    ): array {
        $db       = titancart_db();
        $per_page = max( 1, min( 50, $per_page ) );
        $page     = max( 1, $page );
        $offset   = ( $page - 1 ) * $per_page;

        $order_clause = match ( $sort ) {
            'oldest'  => 'r.created_at ASC',
            'highest' => 'r.rating DESC, r.created_at DESC',
            'lowest'  => 'r.rating ASC, r.created_at DESC',
            'helpful' => 'r.helpful DESC, r.created_at DESC',
            default   => 'r.created_at DESC',   // newest
        };

        $total = (int) $db->getField(
            "SELECT COUNT(*) FROM {$db->table('reviews')} r
             WHERE r.product_id = ?i AND r.status = 'approved'",
            $product_id
        );

        $rows = $db->getArray(
            "SELECT
                r.id,
                r.product_id,
                r.vendor_id,
                r.customer_id,
                r.rating,
                r.vendor_rating,
                r.title,
                r.body,
                r.helpful,
                r.not_helpful,
                r.ip_address,
                r.created_at,
                COALESCE(c.first_name, '') AS author_first,
                COALESCE(c.last_name,  '') AS author_last,
                CASE WHEN r.order_id IS NOT NULL THEN 1 ELSE 0 END AS verified_purchase
             FROM {$db->table('reviews')} r
             LEFT JOIN {$db->table('customers')} c ON c.id = r.customer_id
             WHERE r.product_id = ?i AND r.status = 'approved'
             ORDER BY $order_clause
             LIMIT ?i OFFSET ?i",
            $product_id,
            $per_page,
            $offset
        );

        // Compose a display name, never exposing emails.
        foreach ( $rows as &$row ) {
            $first = trim( $row['author_first'] );
            $last  = trim( $row['author_last'] );
            if ( $first || $last ) {
                // Show first name + last initial for privacy.
                $row['author_name'] = $first . ( $last ? ' ' . mb_substr( $last, 0, 1 ) . '.' : '' );
            } else {
                $row['author_name'] = __( 'Anonymous', 'titancart-lite' );
            }
            unset( $row['author_first'], $row['author_last'], $row['ip_address'] );
        }
        unset( $row );

        return [
            'reviews' => $rows ?: [],
            'total'   => $total,
            'pages'   => $total > 0 ? (int) ceil( $total / $per_page ) : 1,
        ];
    }

    /**
     * Return aggregate rating stats for a product: avg, count, distribution.
     *
     * Uses the denormalised rating_avg / rating_count columns on tc_products
     * for speed, then calculates the distribution breakdown on demand.
     *
     * @param  int $product_id
     * @return array{
     *   avg_rating: float,
     *   total_count: int,
     *   distribution: array<int, int>
     * }
     */
    public function get_review_stats( int $product_id ): array {
        $db = titancart_db();

        // Quick avg + count from denormalised product columns.
        $product = $db->getRow(
            "SELECT rating_avg, rating_count FROM {$db->table('products')} WHERE id = ?i LIMIT 1",
            $product_id
        );

        // Per-star distribution (only from approved reviews).
        $dist_rows = $db->getArray(
            "SELECT rating, COUNT(*) AS cnt
             FROM {$db->table('reviews')}
             WHERE product_id = ?i AND status = 'approved'
             GROUP BY rating",
            $product_id
        );

        $distribution = [ 5 => 0, 4 => 0, 3 => 0, 2 => 0, 1 => 0 ];
        foreach ( $dist_rows as $d ) {
            $star = (int) $d['rating'];
            if ( isset( $distribution[ $star ] ) ) {
                $distribution[ $star ] = (int) $d['cnt'];
            }
        }

        return [
            'avg_rating'   => round( (float) ( $product['rating_avg']   ?? 0 ), 2 ),
            'total_count'  => (int) ( $product['rating_count'] ?? 0 ),
            'distribution' => $distribution,
        ];
    }

    // ══════════════════════════════════════════════════════════════════════════
    // SUBMIT A REVIEW
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Submit a new product review.
     *
     * Runs all validation gates:
     *   1. Module enabled check
     *   2. Guest / login check
     *   3. Purchase requirement check
     *   4. Duplicate review check
     *   5. Field validation (rating 1–5, body not empty)
     *
     * Inserts the review and — if auto_approve is on — immediately calls
     * update_product_rating(). Returns the new review_id or a WP_Error.
     *
     * @param  int        $product_id
     * @param  array      $data        Keyed: rating (int), title (string), body (string)
     * @param  int|null   $customer_id tc_customers.id; null for guests
     * @param  string     $ip          Submitter's IP (sanitised before storage)
     * @return int|\WP_Error
     */
    public function create( int $product_id, array $data, ?int $customer_id = null, string $ip = '' ) {

        // 1. Module must be enabled.
        if ( ! (int) $this->setting( 'reviews_enabled', 1 ) ) {
            return new \WP_Error( 'reviews_disabled', __( 'Product reviews are currently disabled.', 'titancart-lite' ) );
        }

        // 2. Guest restriction.
        if ( ! $customer_id && ! (int) $this->setting( 'reviews_allow_guest', 1 ) ) {
            return new \WP_Error( 'login_required', __( 'You must be logged in to leave a review.', 'titancart-lite' ) );
        }

        // 3. Purchase requirement.
        if ( (int) $this->setting( 'reviews_require_purchase', 0 ) ) {
            if ( ! $customer_id || ! $this->has_purchased( $product_id, $customer_id ) ) {
                return new \WP_Error(
                    'purchase_required',
                    __( 'You must have purchased this product to leave a review.', 'titancart-lite' )
                );
            }
        }

        // 4. Duplicate check (one review per product per customer OR per ip for guests).
        if ( ! $this->can_review( $product_id, $customer_id, $ip ) ) {
            return new \WP_Error(
                'already_reviewed',
                __( 'You have already submitted a review for this product.', 'titancart-lite' )
            );
        }

        // 5. Field validation.
        $rating = (int) ( $data['rating'] ?? 0 );
        $body   = trim( (string) ( $data['body'] ?? '' ) );
        $title  = sanitize_text_field( $data['title'] ?? '' );

        if ( $rating < 1 || $rating > 5 ) {
            return new \WP_Error( 'invalid_rating', __( 'Please select a rating between 1 and 5 stars.', 'titancart-lite' ) );
        }
        if ( $body === '' ) {
            return new \WP_Error( 'empty_body', __( 'Please write a review before submitting.', 'titancart-lite' ) );
        }

        // 5b. Optional vendor rating (S201 Track B Module B9 composite reviews).
        // vendor_rating is independent of `rating`: customers reviewing a
        // vendor-sold product may optionally rate the seller (1-5 stars).
        // NULL when:
        //   - product has no vendor (admin product) — stamper drops it
        //   - customer chose not to rate the vendor (omitted or 0 in submission)
        // vendor_id is resolved at INSERT time by listeners on the new
        // `titancart_review_data` filter (Pattern #158 ninth instance).
        $vendor_rating_raw = $data['vendor_rating'] ?? null;
        $vendor_rating     = null;
        if ( $vendor_rating_raw !== null && $vendor_rating_raw !== '' && (int) $vendor_rating_raw !== 0 ) {
            $vr = (int) $vendor_rating_raw;
            if ( $vr < 1 || $vr > 5 ) {
                return new \WP_Error(
                    'invalid_vendor_rating',
                    __( 'Vendor rating must be between 1 and 5 stars.', 'titancart-lite' )
                );
            }
            $vendor_rating = $vr;
        }

        // Resolve verified purchase order_id.
        $order_id = null;
        if ( $customer_id ) {
            $order_id = $this->get_purchase_order_id( $product_id, $customer_id );
        }

        $auto_approve = (int) $this->setting( 'reviews_auto_approve', 0 );
        $status       = $auto_approve ? 'approved' : 'pending';

        // ─────────────────────────────────────────────────────────────────
        // Pattern #158 ninth instance — `titancart_review_data` filter at
        // write-layer of reviews. Marketplace's VendorReviewDataStamper
        // (S201) subscribes to inject `vendor_id` based on the product's
        // current vendor assignment via VendorRepository::find_by_product_ids.
        // Filter contract: MAY ADD or MUTATE keys; MUST RETURN a complete
        // INSERT-ready array. Listeners that don't recognize the product
        // (e.g. admin products with no vendor) leave $row unchanged. Filter
        // signature mirrors S190's `titancart_order_item_data` shape at
        // write-layer of order_items.
        // ─────────────────────────────────────────────────────────────────
        $insert_data = [
            'product_id'    => $product_id,
            'vendor_id'     => null,
            'customer_id'   => $customer_id,
            'order_id'      => $order_id,
            'rating'        => $rating,
            'vendor_rating' => $vendor_rating,
            'title'         => $title,
            'body'          => wp_kses_post( $body ),
            'status'        => $status,
            'ip_address'    => sanitize_text_field( $ip ),
            'created_at'    => current_time( 'mysql' ),
        ];

        /**
         * Filters the review row before INSERT. Use this to stamp
         * extension-owned columns (e.g. vendor_id from marketplace).
         *
         * @since S201
         *
         * @param array $insert_data Complete INSERT row. MAY add/mutate keys;
         *                           MUST return a complete array.
         * @param int   $product_id  The product being reviewed.
         * @param array $data        Original input data array from caller.
         */
        $insert_data = apply_filters( 'titancart_review_data', $insert_data, $product_id, $data );

        global $wpdb;
        $wpdb->insert(
            $wpdb->prefix . 'tc_reviews',
            $insert_data
        );

        $review_id = (int) $wpdb->insert_id;

        if ( $auto_approve && $review_id ) {
            $this->update_product_rating( $product_id );
        }

        /**
         * Fires after a review has been created.
         *
         * @param int         $review_id
         * @param int         $product_id
         * @param int|null    $customer_id
         * @param string      $status      'approved' or 'pending'
         */
        do_action( 'titancart_review_created', $review_id, $product_id, $customer_id, $status );

        return $review_id;
    }

    // ══════════════════════════════════════════════════════════════════════════
    // ELIGIBILITY CHECKS
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Check whether a user/guest is eligible to post a review.
     *
     * Returns false if a review already exists for this product from the
     * same customer_id (logged-in) or same IP (guest).
     *
     * @param  int         $product_id
     * @param  int|null    $customer_id
     * @param  string|null $ip
     * @return bool
     */
    public function can_review( int $product_id, ?int $customer_id = null, ?string $ip = null ): bool {
        $db = titancart_db();

        if ( $customer_id ) {
            $exists = $db->getField(
                "SELECT id FROM {$db->table('reviews')}
                 WHERE product_id = ?i AND customer_id = ?i LIMIT 1",
                $product_id,
                $customer_id
            );
            return ! $exists;
        }

        // Guest: check by IP only when the store has enabled IP-based deduplication.
        // Default is off — avoids false 409s in testing environments and for NAT users.
        if ( $ip && (int) $this->setting( 'reviews_ip_check', 0 ) ) {
            $exists = $db->getField(
                "SELECT id FROM {$db->table('reviews')}
                 WHERE product_id = ?i AND customer_id IS NULL AND ip_address = ?s LIMIT 1",
                $product_id,
                $ip
            );
            return ! $exists;
        }

        return true;
    }

    /**
     * Check whether a customer has at least one completed order containing this product.
     *
     * Joins tc_orders → tc_order_statuses (handle = 'completed') → tc_order_items.
     *
     * @param  int $product_id
     * @param  int $customer_id
     * @return bool
     */
    public function has_purchased( int $product_id, int $customer_id ): bool {
        $db = titancart_db();
        $id = $db->getField(
            "SELECT oi.id
             FROM {$db->table('order_items')} oi
             JOIN {$db->table('orders')} o ON o.id = oi.order_id
             JOIN {$db->table('order_statuses')} os ON os.id = o.status_id
             WHERE oi.product_id = ?i
               AND o.customer_id = ?i
               AND os.handle = 'completed'
             LIMIT 1",
            $product_id,
            $customer_id
        );
        return (bool) $id;
    }

    /**
     * Return the order_id of the customer's first completed order containing the product.
     *
     * Used to set the verified-purchase linkage on new reviews.
     *
     * @param  int $product_id
     * @param  int $customer_id
     * @return int|null
     */
    private function get_purchase_order_id( int $product_id, int $customer_id ): ?int {
        $db = titancart_db();
        $id = $db->getField(
            "SELECT o.id
             FROM {$db->table('orders')} o
             JOIN {$db->table('order_statuses')} os ON os.id = o.status_id
             JOIN {$db->table('order_items')} oi ON oi.order_id = o.id
             WHERE o.customer_id = ?i
               AND oi.product_id = ?i
               AND os.handle = 'completed'
             ORDER BY o.created_at ASC
             LIMIT 1",
            $customer_id,
            $product_id
        );
        return $id ? (int) $id : null;
    }

    // ══════════════════════════════════════════════════════════════════════════
    // HELPFUL VOTES
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Record a helpful/not-helpful vote on a review.
     *
     * Prevents double-voting via UNIQUE keys on (review_id, customer_id) and
     * (review_id, ip_address). After a successful insert, updates the counter
     * columns on tc_reviews.
     *
     * @param  int         $review_id
     * @param  int         $vote        1 = helpful, 0 = not helpful
     * @param  int|null    $customer_id
     * @param  string      $ip
     * @return true|\WP_Error
     */
    public function vote( int $review_id, int $vote, ?int $customer_id = null, string $ip = '' ) {
        $db = titancart_db();

        // Verify the review exists and is approved.
        $review = $db->getRow(
            "SELECT id, product_id, helpful, not_helpful FROM {$db->table('reviews')}
             WHERE id = ?i AND status = 'approved' LIMIT 1",
            $review_id
        );
        if ( ! $review ) {
            return new \WP_Error( 'not_found', __( 'Review not found.', 'titancart-lite' ) );
        }

        // Check for existing vote (by customer_id OR ip).
        $already = false;
        if ( $customer_id ) {
            $already = (bool) $db->getField(
                "SELECT id FROM {$db->table('review_votes')} WHERE review_id = ?i AND customer_id = ?i LIMIT 1",
                $review_id,
                $customer_id
            );
        }
        if ( ! $already && $ip ) {
            $already = (bool) $db->getField(
                "SELECT id FROM {$db->table('review_votes')} WHERE review_id = ?i AND ip_address = ?s LIMIT 1",
                $review_id,
                $ip
            );
        }

        if ( $already ) {
            return new \WP_Error( 'already_voted', __( 'You have already voted on this review.', 'titancart-lite' ) );
        }

        global $wpdb;
        $inserted = $wpdb->insert(
            $wpdb->prefix . 'tc_review_votes',
            [
                'review_id'   => $review_id,
                'customer_id' => $customer_id,
                'ip_address'  => sanitize_text_field( $ip ),
                'vote'        => $vote ? 1 : 0,
                'created_at'  => current_time( 'mysql' ),
            ]
        );

        if ( ! $inserted ) {
            // Unique constraint fired — race condition, treat as already voted.
            return new \WP_Error( 'already_voted', __( 'You have already voted on this review.', 'titancart-lite' ) );
        }

        // Update the denormalised counters.
        $col = $vote ? 'helpful' : 'not_helpful';
        $wpdb->query( // phpcs:ignore PluginCheck.Security.DirectDB.UnescapedDBParameter -- $col is a two-literal ternary from a bool ('helpful'/'not_helpful'), never user input; table name is $wpdb->prefix-built; id bound via placeholder.
            $wpdb->prepare(
                "UPDATE {$wpdb->prefix}tc_reviews SET `$col` = `$col` + 1 WHERE id = %d",
                $review_id
            )
        );

        /**
         * Fires after a helpful vote is recorded.
         *
         * @param int      $review_id
         * @param int      $vote         1 = helpful, 0 = not helpful
         * @param int|null $customer_id
         */
        do_action( 'titancart_review_voted', $review_id, $vote, $customer_id );

        return true;
    }

    // ══════════════════════════════════════════════════════════════════════════
    // ADMIN — MODERATION
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Approve or reject a review.
     *
     * After approval, recalculates the product's rating average and count.
     * After rejection/spam the product rating is also recalculated in case
     * the review was previously approved.
     *
     * @param  int    $review_id
     * @param  string $status  approved|rejected|spam
     * @return true|\WP_Error
     */
    public function moderate( int $review_id, string $status ) {
        $allowed = [ 'approved', 'rejected', 'spam' ];
        if ( ! in_array( $status, $allowed, true ) ) {
            return new \WP_Error( 'invalid_status', __( 'Invalid review status.', 'titancart-lite' ) );
        }

        $db = titancart_db();
        $review = $db->getRow(
            "SELECT id, product_id, status AS current_status FROM {$db->table('reviews')} WHERE id = ?i LIMIT 1",
            $review_id
        );
        if ( ! $review ) {
            return new \WP_Error( 'not_found', __( 'Review not found.', 'titancart-lite' ) );
        }

        global $wpdb;
        $wpdb->update(
            $wpdb->prefix . 'tc_reviews',
            [ 'status' => $status ],
            [ 'id'     => $review_id ]
        );

        // Recalculate product rating whenever approval status changes.
        $this->update_product_rating( (int) $review['product_id'] );

        /**
         * Fires after a review's moderation status changes.
         *
         * @param int    $review_id
         * @param string $status
         * @param string $previous_status
         */
        do_action( 'titancart_review_moderated', $review_id, $status, $review['current_status'] );

        return true;
    }

    /**
     * Permanently delete a review and its associated votes.
     *
     * @param  int $review_id
     * @return true|\WP_Error
     */
    public function delete( int $review_id ) {
        $db     = titancart_db();
        $review = $db->getRow(
            "SELECT id, product_id FROM {$db->table('reviews')} WHERE id = ?i LIMIT 1",
            $review_id
        );
        if ( ! $review ) {
            return new \WP_Error( 'not_found', __( 'Review not found.', 'titancart-lite' ) );
        }

        global $wpdb;
        $wpdb->delete( $wpdb->prefix . 'tc_review_votes', [ 'review_id' => $review_id ] );
        $wpdb->delete( $wpdb->prefix . 'tc_reviews',      [ 'id'        => $review_id ] );

        $this->update_product_rating( (int) $review['product_id'] );

        do_action( 'titancart_review_deleted', $review_id, (int) $review['product_id'] );

        return true;
    }

    /**
     * Return paginated reviews in the admin queue with optional filters.
     *
     * Supported $args keys:
     *   status     string   pending|approved|rejected|spam (omit for all)
     *   product_id int
     *   rating     int      1–5
     *   date_from  string   YYYY-MM-DD
     *   date_to    string   YYYY-MM-DD
     *   page       int
     *   per_page   int
     *
     * @param  array $args
     * @return array{ reviews: array, total: int, pages: int }
     */
    public function get_all_reviews( array $args = [] ): array {
        $db       = titancart_db();
        $page     = max( 1, (int) ( $args['page']     ?? 1 ) );
        $per_page = min( 100, max( 1, (int) ( $args['per_page'] ?? 20 ) ) );
        $offset   = ( $page - 1 ) * $per_page;

        $where  = [];
        $params = [];

        if ( ! empty( $args['status'] ) ) {
            $where[]  = 'r.status = ?s';
            $params[] = $args['status'];
        }
        if ( ! empty( $args['product_id'] ) ) {
            $where[]  = 'r.product_id = ?i';
            $params[] = (int) $args['product_id'];
        }
        if ( ! empty( $args['rating'] ) ) {
            $where[]  = 'r.rating = ?i';
            $params[] = (int) $args['rating'];
        }
        if ( ! empty( $args['date_from'] ) ) {
            $where[]  = 'r.created_at >= ?s';
            $params[] = $args['date_from'] . ' 00:00:00';
        }
        if ( ! empty( $args['date_to'] ) ) {
            $where[]  = 'r.created_at <= ?s';
            $params[] = $args['date_to'] . ' 23:59:59';
        }

        $where_sql = $where ? 'WHERE ' . implode( ' AND ', $where ) : '';

        $count_params = $params;
        $total = (int) $db->getField(
            "SELECT COUNT(*) FROM {$db->table('reviews')} r $where_sql",
            ...$count_params
        );

        $list_params   = $params;
        $list_params[] = $per_page;
        $list_params[] = $offset;

        $rows = $db->getArray(
            "SELECT
                r.id, r.product_id, r.vendor_id, r.customer_id, r.rating, r.vendor_rating,
                r.title, r.body,
                r.status, r.helpful, r.not_helpful, r.created_at,
                CASE WHEN r.order_id IS NOT NULL THEN 1 ELSE 0 END AS verified_purchase,
                COALESCE(c.first_name, '') AS author_first,
                COALESCE(c.last_name,  '') AS author_last,
                COALESCE(c.email, '')       AS author_email,
                COALESCE(pt.name, '')       AS product_name
             FROM {$db->table('reviews')} r
             LEFT JOIN {$db->table('customers')} c ON c.id = r.customer_id
             LEFT JOIN {$db->table('product_translations')} pt
                ON pt.product_id = r.product_id AND pt.language_code = 'en'
             $where_sql
             ORDER BY r.created_at DESC
             LIMIT ?i OFFSET ?i",
            ...$list_params
        );

        foreach ( $rows as &$row ) {
            $row['author_name'] = trim( $row['author_first'] . ' ' . $row['author_last'] ) ?: __( 'Anonymous', 'titancart-lite' );
            unset( $row['author_first'], $row['author_last'] );
        }
        unset( $row );

        return [
            'reviews' => $rows ?: [],
            'total'   => $total,
            'pages'   => $total > 0 ? (int) ceil( $total / $per_page ) : 1,
        ];
    }

    /**
     * Convenience wrapper: pending moderation queue.
     *
     * @param  int $page
     * @param  int $per_page
     * @return array
     */
    public function get_pending_reviews( int $page = 1, int $per_page = 20 ): array {
        return $this->get_all_reviews( [
            'status'   => 'pending',
            'page'     => $page,
            'per_page' => $per_page,
        ] );
    }

    // ══════════════════════════════════════════════════════════════════════════
    // RATING RECALCULATION
    // ══════════════════════════════════════════════════════════════════════════

    /**
     * Recalculate and persist the product's aggregate rating.
     *
     * Reads all approved reviews, computes the average, then writes
     * rating_avg and rating_count to tc_products in a single UPDATE.
     *
     * @param  int $product_id
     * @return void
     */
    public function update_product_rating( int $product_id ): void {
        $db  = titancart_db();
        $row = $db->getRow(
            "SELECT COUNT(*) AS cnt, COALESCE(AVG(rating), 0) AS avg
             FROM {$db->table('reviews')}
             WHERE product_id = ?i AND status = 'approved'",
            $product_id
        );

        $count = (int) ( $row['cnt'] ?? 0 );
        $avg   = round( (float) ( $row['avg'] ?? 0 ), 2 );

        global $wpdb;
        $wpdb->update(
            $wpdb->prefix . 'tc_products',
            [
                'rating_avg'   => $avg,
                'rating_count' => $count,
            ],
            [ 'id' => $product_id ]
        );

        /**
         * Fires after a product's aggregate rating is recalculated.
         *
         * @param int   $product_id
         * @param float $avg
         * @param int   $count
         */
        do_action( 'titancart_product_rating_updated', $product_id, $avg, $count );
    }
}
