<?php
/**
 * Wishlist class.
 *
 * @package SaveToWishlist\Classes\Frontend
 * @since 1.0.0
 */
namespace SaveToWishlist\Classes\Frontend;

use SaveToWishlist\Abstracts\Singleton;
use SaveToWishlist\Helpers\Helper_Functions;
use SaveToWishlist\Classes\Factories\Collections;
use SaveToWishlist\Classes\Factories\Collection;

/**
 * Class Wishlist
 *
 * This class handles the wishlist functionality.
 */
class Wishlist extends Singleton {

    /**
     * Helper functions.
     *
     * @var array
     */
    private $helper_functions;

    /**
     * Current wishlist data for title and meta description.
     *
     * @var object|null
     */
    private $current_wishlist_data;

    /**
     * Request-scoped cache for repeated wishlist reads.
     *
     * Shop and catalog pages call get_default_wishlist(), get_wishlists_as_options()
     * and the shop-loop variant of check_if_product_in_wishlist() once per product
     * card. The underlying queries return identical user-scoped data for the duration
     * of the request, so we memoize them here to collapse what was previously an
     * N+1 pattern. Cleared by clear_request_cache() on any wishlist mutation so a
     * write-then-read flow in the same request never sees stale data.
     *
     * Declared static so the cache spans the request even when multiple Wishlist
     * objects coexist — the Singleton base shares a single $instance slot across
     * subclasses, so interleaved Module::instance() calls can produce more than one
     * live Wishlist per request. A per-instance cache would let cross-module callers
     * (e.g. Buttons + Frontend_Hooks) re-run the same SELECTs; the static slot keeps
     * the dedup correct regardless of how many instances exist.
     *
     * @since 1.0.7
     * @var array
     */
    private static $request_cache = array();

    /**
     * Constructor.
     *
     * @since 1.0.0
     * @access public
     */
    public function __construct() {
        $this->helper_functions = Helper_Functions::instance();

        // Invalidate the request-scoped cache whenever wishlist data mutates.
        // Subscribed at both the high-level (AJAX/admin) and low-level (Collections
        // factory) layers so REST writes, auto-remove on add-to-cart, and any other
        // path that goes straight through Collections cannot leave stale reads.
        $invalidation_hooks = array(
            'stwlite_after_product_added_to_wishlist',
            'stwlite_after_product_removed_from_wishlist',
            'stwlite_after_product_updated',
            'stwlite_after_save_collection',
            'stwlite_after_delete_collections',
            'stwlite_after_insert_collection_item',
            'stwlite_after_update_collection_item',
            'stwlite_after_move_collection_item',
            'stwlite_after_update_collection_quantity_item',
            'stwlite_after_delete_collection_items',
            'stwlite_after_delete_collection_items_by_user',
            'stwlite_after_delete_collection_items_by_admin',
        );
        foreach ( $invalidation_hooks as $invalidation_hook ) {
            add_action( $invalidation_hook, array( $this, 'clear_request_cache' ) );
        }
    }

    /**
     * Clear the request-scoped cache populated by the memoized getters below.
     *
     * @since 1.0.7
     * @access public
     */
    public function clear_request_cache() {
        self::$request_cache = array();
    }

    /**
     * Return the cached value for $key, computing it via $callback on first miss.
     *
     * @param string   $key      Request-cache slot.
     * @param callable $callback Producer invoked only on cache miss.
     *
     * @since 1.0.7
     * @access private
     *
     * @return mixed
     */
    private function remember( $key, callable $callback ) {
        if ( ! array_key_exists( $key, self::$request_cache ) ) {
            self::$request_cache[ $key ] = $callback();
        }

        return self::$request_cache[ $key ];
    }

    /**
     * Get the wishlist options.
     *
     * @since 1.0.0
     * @access public
     * @return array The wishlist options.
     */
    public function get_wishlist_options() {
        $wishlist_title         = $this->helper_functions->get_string_option( 'stwlite_settings_wishlist_title', __( 'My Wishlists', 'saveto-wishlist-lite-for-woocommerce' ) );
        $wishlist_description   = $this->helper_functions->get_string_option( 'stwlite_settings_wishlist_description', __( 'Manage your wishlists', 'saveto-wishlist-lite-for-woocommerce' ) );
        $disable_guest_wishlist = get_option( 'stwlite_settings_disable_guest_wishlist', 'no' );
        $show_login_create_link = get_option( 'stwlite_settings_show_login_create_link', 'no' );

        $options = array(
            'title'                           => $wishlist_title,
            'description'                     => $wishlist_description,
            'disableGuestWishlist'            => $disable_guest_wishlist,
            'showLoginCreateLink'             => $show_login_create_link,
            'defaultWishlist'                 => $this->get_default_wishlist( true ),
            'wishlistUrl'                     => get_permalink( get_option( 'stwlite_wishlist_page' ) ),
            'wishlistAddActionButton'         => get_option( 'stwlite_settings_wishlist_add_action_button', 'redirect' ),
            'addCartProductsToWishlist'       => get_option( 'stwlite_settings_add_cart_products_to_wishlist', 'yes' ),
            'autoRemoveProductFromWishlist'   => get_option( 'stwlite_settings_auto_remove_product_from_wishlist', 'yes' ),
            'showLoginLink'                   => get_option( 'stwlite_settings_show_login_link', 'no' ),
            'allowRemoveProductFromWishlist'  => apply_filters( 'stwlite_allow_remove_product_from_wishlist', true ),
            'allowMultipleProductsInWishlist' => apply_filters( 'stwlite_allow_multiple_products_in_wishlist', false ),
            'myAccountUrl'                    => get_permalink( get_option( 'woocommerce_myaccount_page_id' ) ),
            'allowMultipleWishlists'          => get_option( 'stwlite_settings_allow_multiple_wishlists', 'yes' ),
        );

        $options = apply_filters( 'stwlite_options', $options );

        return $options;
    }

    /**
     * Saveto wishlist shortcodes.
     *
     * @param array $atts The attributes.
     *
     * @since 1.0.0
     * @access public
     */
    public function saveto_wishlist_shortcodes( $atts ) {

        ob_start();

        $atts = shortcode_atts(
            array(
                'id' => '',
            ),
            $atts,
            'saveto_wishlist'
        );

        $disable_guest_wishlist = get_option( 'stwlite_settings_disable_guest_wishlist', 'no' );
        $is_user_logged_in      = is_user_logged_in();

        $wishlist_code = $this->get_wishlist_code();
        $attributes    = array();
        if ( ! empty( $atts['id'] ) || ! empty( $wishlist_code ) ) {
            if ( ! empty( $atts['id'] ) ) {
                $collection = Collections::instance()->get_collection( $atts['id'] );
            } elseif ( ! empty( $wishlist_code ) ) {
                $collection = Collections::instance()->get_collection( $wishlist_code, 'url_code' );
            }

            if ( ! empty( $collection ) ) {
                $data = Collection::get_instance( $collection );
            }

            if ( ! empty( $data ) ) {
                $attributes['data-id'] = $data->id;
            }
        }

        if ( ! empty( $wishlist_code ) ) {
            echo '<saveto-wishlist-item ' . esc_attr( $this->helper_functions->format_attributes( $attributes ) ) . '></saveto-wishlist-item>';
        } elseif ( 'no' === $disable_guest_wishlist && ! $is_user_logged_in ) {
            echo '<saveto-wishlist-guest></saveto-wishlist-guest>';
        } else {
            echo '<saveto-wishlist ' . esc_attr( $this->helper_functions->format_attributes( $attributes ) ) . '></saveto-wishlist>';
        }

        return ob_get_clean();
    }

    /**
     * Rewrite wishlist URL to use a redirect page with a unique code from wishlist id.
     *
     * @param string     $url The original wishlist URL.
     * @param int|string $id The wishlist ID.
     * @return string The rewritten URL.
     */
    public function rewrite_wishlist_url( $url, $id ) {
        $collection  = Collections::instance()->get_collection( $id );
        $unique_code = $collection->url_code;

        $redirect_page_id = get_option( 'stwlite_wishlist_page' );
        if ( $redirect_page_id ) {
            // Get translated page ID for current language.
            $translated_page_id = apply_filters( 'wpml_object_id', $redirect_page_id, 'page', true );
            $redirect_url       = get_permalink( $translated_page_id ? $translated_page_id : $redirect_page_id );
        } else {
            $redirect_url = home_url( '/my-account/' );
        }

        $rewritten_url = add_query_arg( array( 'wishlist_code' => $unique_code ), $redirect_url );

        return $rewritten_url;
    }

    /**
     * Add rewrite rules.
     *
     * @since 1.0.0
     * @access public
     */
    public function add_rewrite_rules() {
        $redirect_page_id = get_option( 'stwlite_wishlist_page' );

        if ( ! $redirect_page_id ) {
            add_rewrite_rule( 'my-account/([a-zA-Z0-9_-]+)/?', 'index.php?wishlist_code=$matches[1]', 'top' );
            return;
        }

        // Check if WPML is active.
        if ( function_exists( 'icl_get_languages' ) ) {
            $languages = icl_get_languages( 'skip_missing=0' );

            foreach ( $languages as $language ) {
                // Get translated page ID for each language.
                $translated_page_id = apply_filters( 'wpml_object_id', $redirect_page_id, 'page', false, $language['language_code'] );

                if ( $translated_page_id ) {
                    $slug = get_post_field( 'post_name', $translated_page_id );

                    if ( $slug ) {
                        // WPML handles language prefixes automatically, so we only need the slug pattern.
                        // This works for all WPML URL formats (directory, domain, parameter).
                        add_rewrite_rule( $slug . '/([a-zA-Z0-9_-]+)/?', 'index.php?wishlist_code=$matches[1]', 'top' );
                    }
                }
            }
        } else {
            // No WPML, use default slug.
            $slug = get_post_field( 'post_name', $redirect_page_id );
            if ( $slug ) {
                add_rewrite_rule( $slug . '/([a-zA-Z0-9_-]+)/?', 'index.php?wishlist_code=$matches[1]', 'top' );
            }
        }
    }

    /**
     * Get wishlist code from query var or URI (with WPML fallback).
     *
     * @since 1.0.0
     * @access public
     * @return string|false The wishlist code or false if not found.
     */
    public function get_wishlist_code() {
        $code = get_query_var( 'wishlist_code' );

        if ( empty( $code ) ) {
            // Fallback: parse from URI (useful for WPML compatibility).
            $code = $this->get_wishlist_code_from_uri();

            // If found in URI, set it as query var for consistency.
            if ( $code ) {
                set_query_var( 'wishlist_code', $code );
            }
        }

        return $code ? $code : false;
    }

    /**
     * Get wishlist code from request URI (fallback for WPML compatibility).
     *
     * @since 1.0.0
     * @access private
     * @return string|false The wishlist code or false if not found.
     */
    private function get_wishlist_code_from_uri() {
        $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';

        if ( empty( $request_uri ) ) {
            return false;
        }

        // Remove query string if present.
        $request_uri = strtok( $request_uri, '?' );

        // Remove leading slash and split by slashes.
        $uri_parts = array_filter( explode( '/', trim( $request_uri, '/' ) ) );
        $uri_parts = array_values( $uri_parts );

        if ( empty( $uri_parts ) ) {
            return false;
        }

        // Get all possible wishlist page slugs.
        $redirect_page_id = get_option( 'stwlite_wishlist_page' );
        $wishlist_slugs   = array();

        if ( $redirect_page_id ) {
            if ( function_exists( 'icl_get_languages' ) ) {
                // Get all translated slugs.
                $languages = icl_get_languages( 'skip_missing=0' );
                foreach ( $languages as $language ) {
                    $translated_page_id = apply_filters( 'wpml_object_id', $redirect_page_id, 'page', false, $language['language_code'] );
                    if ( $translated_page_id ) {
                        $slug = get_post_field( 'post_name', $translated_page_id );
                        if ( $slug ) {
                            $wishlist_slugs[] = $slug;
                        }
                    }
                }
            } else {
                $slug = get_post_field( 'post_name', $redirect_page_id );
                if ( $slug ) {
                    $wishlist_slugs[] = $slug;
                }
            }
        } else {
            $wishlist_slugs[] = 'my-account';
        }

        // Check if any wishlist slug is in the URI and extract the code after it.
        foreach ( $wishlist_slugs as $wishlist_slug ) {
            $slug_index = array_search( $wishlist_slug, $uri_parts, true );
            if ( false !== $slug_index && isset( $uri_parts[ $slug_index + 1 ] ) ) {
                $code = $uri_parts[ $slug_index + 1 ];
                // Validate code format (alphanumeric, underscore, hyphen).
                if ( preg_match( '/^[a-zA-Z0-9_-]+$/', $code ) ) {
                    return $code;
                }
            }
        }

        return false;
    }

    /**
     * Display content for the wishlist rewrite rule page using the current page name.
     *
     * @param string $template The template.
     *
     * @since 1.0.0
     * @access public
     */
    public function display_wishlist_rewrite_content( $template ) {
        // Get wishlist code with WPML fallback.
        $code = $this->get_wishlist_code();

        if ( $code ) {
            $current_user_id = get_current_user_id();
            $collection      = Collections::instance()->get_collection( $code, 'url_code' );

            // Redirect immediately when the wishlist does not exist.
            if ( empty( $collection ) ) {
                wp_safe_redirect( get_permalink( get_option( 'stwlite_wishlist_page' ) ) );
                exit;
            }

            $data = Collection::get_instance( $collection );

            $share_wishlist_checker = apply_filters( 'stwlite_share_wishlist_checker', false, $data, $current_user_id );

            if ( ! current_user_can( 'manage_options' ) && empty( $share_wishlist_checker ) ) {
                $wishlist_page = get_permalink( get_option( 'stwlite_wishlist_page' ) );
                $is_owner      = (int) $data->user_id === $current_user_id;

                // Draft wishlists are only accessible to their owner.
                if ( 'publish' !== $data->status && ! $is_owner ) {
                    wp_safe_redirect( $wishlist_page );
                    exit;
                }

                // Non-owners can only access public wishlists.
                if ( ! $is_owner && 0 === (int) $data->is_public ) {
                    wp_safe_redirect( $wishlist_page );
                    exit;
                }
            }

            // Store wishlist data for title and meta description.
            $this->current_wishlist_data = $data;

            if ( ! empty( $data ) ) {
                $title = $data->name;
            }

            $parent_page   = get_page_by_path( 'my-account' );
            $wishlist_page = get_option( 'stwlite_wishlist_page' );
            if ( ! empty( $wishlist_page ) ) {
                $parent_page = get_post( $wishlist_page );
            }

            if ( ! empty( $parent_page ) ) {
                global $wp_query, $post;
                $wp_query->is_404 = false;
                status_header( 200 );

                $wp_query->is_page       = true;
                $wp_query->is_singular   = true;
                $wp_query->is_home       = false;
                $wp_query->found_posts   = 1;
                $wp_query->post_count    = 1;
                $wp_query->max_num_pages = 1;

                // Use parent page's content and template.
                $wp_query->post              = $parent_page;
                $wp_query->posts             = array( $parent_page );
                $wp_query->queried_object    = $parent_page;
                $wp_query->queried_object_id = $parent_page->ID;
                $wp_query->post_title        = $parent_page->post_title;

                // Set global $post to ensure template functions work correctly.
                // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
                $post = $parent_page;
                setup_postdata( $post );

                // Add filters for title tag and meta description.
                add_filter( 'document_title_parts', array( $this, 'modify_wishlist_title' ), 10, 1 );
                add_action( 'wp_head', array( $this, 'add_wishlist_meta_description' ), 1 );
            }
        }
    }

    /**
     * Modify the document title for wishlist pages.
     *
     * @param array $title_parts The title parts.
     *
     * @since 1.0.0
     * @access public
     * @return array The modified title parts.
     */
    public function modify_wishlist_title( $title_parts ) {
        if ( ! empty( $this->current_wishlist_data ) && ! empty( $this->current_wishlist_data->name ) ) {
            $title_parts['title'] = $this->current_wishlist_data->name;
        }

        return $title_parts;
    }

    /**
     * Add meta description for wishlist pages.
     *
     * @since 1.0.0
     * @access public
     */
    public function add_wishlist_meta_description() {
        if ( ! empty( $this->current_wishlist_data ) ) {
            $description = '';

            if ( ! empty( $this->current_wishlist_data->description ) ) {
                $description = wp_strip_all_tags( $this->current_wishlist_data->description );
            } elseif ( ! empty( $this->current_wishlist_data->name ) ) {
                // Fallback to wishlist name if no description.
                $description = sprintf(
                    /* translators: %s: Wishlist name */
                    __( 'View %s wishlist', 'saveto-wishlist-lite-for-woocommerce' ),
                    $this->current_wishlist_data->name
                );
            }

            if ( ! empty( $description ) ) {
                $description = wp_trim_words( $description, 30, '...' );
                echo '<meta name="description" content="' . esc_attr( $description ) . '" />' . "\n";
            }
        }
    }

    /**
     * Add query vars.
     *
     * @param array $query_vars The query vars.
     *
     * @since 1.0.0
     * @access public
     * @return array The query vars.
     */
    public function add_query_vars( $query_vars ) {
        $query_vars[] = 'wishlist_code';
        return $query_vars;
    }

    /**
     * Get the wishlist by code.
     *
     * @since 1.0.0
     * @access public
     * @return object The wishlist data.
     */
    public function get_wishlist_by_code() {
        $code = $this->get_wishlist_code();

        if ( ! $code ) {
            return '';
        }

        $collection = Collections::instance()->get_collection( $code, 'url_code' );
        $data       = Collection::get_instance( $collection );

        // Filter wishlist data.
        $data = apply_filters( 'stwlite_data', $data, $collection, $code );

        return $data;
    }

    /**
     * Add to wishlist.
     *
     * @since 1.0.0
     * @access public
     */
    public function add_to_wishlist() {
        if ( ! check_ajax_referer( 'stwlite_frontend_nonce', 'nonce', false ) ) {
            wp_send_json(
                array(
                    'status'  => 'error',
                    'message' => __( 'Invalid process. Please try again.', 'saveto-wishlist-lite-for-woocommerce' ),
                )
            );
        }

        $product_id    = ! empty( $_POST['product_id'] ) ? (int) $_POST['product_id'] : 0;
        $collection_id = ! empty( $_POST['collection_id'] ) ? (int) $_POST['collection_id'] : 0;
        $user_id       = ! empty( $_POST['user_id'] ) ? (int) $_POST['user_id'] : get_current_user_id();
        $quantity      = ! empty( $_POST['quantity'] ) ? (int) $_POST['quantity'] : 1;

        do_action( 'stwlite_before_product_added_to_wishlist', $product_id, $quantity, $collection_id, $user_id );

        $this->add_product_item_to_wishlist( $product_id, $quantity, $collection_id, $user_id );

        do_action( 'stwlite_after_product_added_to_wishlist', $product_id, $quantity, $collection_id, $user_id );

        wp_send_json(
            array(
                'status'  => 'success',
                'message' => __( 'Product added to cart successfully.', 'saveto-wishlist-lite-for-woocommerce' ),
            )
        );
    }

    /**
     * Add product cart to wishlist.
     *
     * @since 1.0.0
     * @access public
     */
    public function add_product_cart_to_wishlist() {
        if ( ! check_ajax_referer( 'stwlite_frontend_nonce', 'nonce', false ) ) {
            wp_send_json(
                array(
                    'status'  => 'error',
                    'message' => __( 'Invalid process. Please try again.', 'saveto-wishlist-lite-for-woocommerce' ),
                )
            );
        }

        $collection_id = ! empty( $_POST['collection_id'] ) ? (int) $_POST['collection_id'] : 0;

        // Optional: specific product IDs to save (used when cart item selection is enabled).
        $selected_ids = array();
        if ( ! empty( $_POST['product_ids'] ) && is_array( $_POST['product_ids'] ) ) {
            $selected_ids = array_values( array_filter( array_map( 'absint', $_POST['product_ids'] ) ) );
        }

        // Get the cart items.
        $cart_items = WC()->cart->get_cart();

        do_action( 'stwlite_before_product_cart_added_to_wishlist', $cart_items, $collection_id );

        $added   = 0;
        $skipped = 0;

        if ( ! empty( $cart_items ) ) {
            foreach ( $cart_items as $cart_item ) {
                $product_id   = (int) $cart_item['product_id'];
                $quantity     = (int) $cart_item['quantity'];
                $effective_id = Helper_Functions::get_effective_product_id( $cart_item );
                $variation_id = $effective_id !== $product_id ? $effective_id : 0;

                // When specific IDs are requested, skip items not in the selection.
                if ( ! empty( $selected_ids ) && ! in_array( $effective_id, $selected_ids, true ) ) {
                    continue;
                }

                // Resolve parent and variation for wishlist duplicate check.
                $product_obj = wc_get_product( $effective_id );
                $parent_id   = Helper_Functions::get_parent_product_id( $product_obj, $product_id );

                // Skip items already present in the wishlist.
                $wishlist_products = $this->check_if_product_in_wishlist( $parent_id, $collection_id, false );
                $in_wishlist       = $this->product_check_in_wishlist( $product_obj, $wishlist_products, $variation_id, is_user_logged_in() );

                if ( $in_wishlist ) {
                    ++$skipped;
                    continue;
                }

                $this->add_product_item_to_wishlist( $effective_id, $quantity, $collection_id );
                ++$added;
            }
        }

        do_action( 'stwlite_after_product_cart_added_to_wishlist', $cart_items, $collection_id );

        wp_send_json(
            array(
                'status'          => 'success',
                'message'         => __( 'Cart items added to the wishlist.', 'saveto-wishlist-lite-for-woocommerce' ),
                'added'           => $added,
                'skipped'         => $skipped,
                'all_in_wishlist' => 0 === $added && $skipped > 0,
            )
        );
    }

    /**
     * Add product item to wishlist.
     *
     * @param int $product_id The product ID.
     * @param int $quantity The quantity.
     * @param int $collection_id The collection ID.
     * @param int $user_id The user ID.
     *
     * @since 1.0.0
     * @access public
     */
    public function add_product_item_to_wishlist( $product_id, $quantity = 1, $collection_id = 0, $user_id = 0 ) {
        if ( 0 === $user_id ) {
            $user_id = get_current_user_id();
        }

        if ( 0 === $collection_id ) {
            // Draft wishlists are not valid save targets, even if marked default.
            $collection = Collections::instance()->get_collections(
                array(
                    'user_id'    => $user_id,
                    'is_default' => 1,
                    'status'     => 'publish',
                )
            );

            if ( ! empty( $collection['data'] ) ) {
                $collection_id = $collection['data'][0]->id;
            }
        }

        do_action( 'stwlite_before_product_updated', $product_id, $quantity, $collection_id, $user_id );

        $variation_id = 0;

        // Check if product is variation or simple product.
        $product   = wc_get_product( $product_id );
        $parent_id = Helper_Functions::get_parent_product_id( $product, $product_id );
        if ( $product && $product->is_type( 'variation' ) ) {
            $variation_id = $product_id;
        }

        // Resolve target-wishlist membership up front so both the reassignment branch
        // and the main add/update path below can reuse the same result.
        $wishlist_products = $this->check_if_product_in_wishlist( $parent_id, $collection_id, false );
        $in_wishlist       = $this->product_check_in_wishlist( $product, $wishlist_products, $variation_id, is_user_logged_in() );

        // If the product already lives in a draft collection, move it to the
        // target collection instead of inserting a duplicate row.
        $draft_item = Collections::instance()->get_item_in_draft_collection( $parent_id, $variation_id, $user_id );
        if ( ! empty( $draft_item ) ) {
            $source_collection_id = (int) $draft_item->collection_id;

            if ( $in_wishlist ) {
                // Target already has this (product_id, variation_id) row; reassigning would
                // collide on the unique key. Delete the draft row by its primary key so we
                // never touch unrelated variation rows that share the same parent product_id.
                Collections::instance()->delete_collection_item_by_id( (int) $draft_item->id );
            } else {
                Collections::instance()->save_collection_item(
                    array(
                        'id'            => (int) $draft_item->id,
                        'collection_id' => $collection_id,
                    )
                );

                // save_collection_item() does not invalidate the per-collection items cache;
                // flush the target collection so stale reads cannot show the just-moved row.
                wp_cache_delete( 'collection_items_' . (int) $collection_id, STWLITE_CACHE_GROUP );
            }

            // Flush the source (draft) collection cache for both paths; its row was
            // either deleted or reassigned, so the cached list is now stale.
            wp_cache_delete( 'collection_items_' . $source_collection_id, STWLITE_CACHE_GROUP );

            if ( ! $in_wishlist ) {
                do_action( 'stwlite_after_product_updated', $parent_id, $collection_id, $quantity, $user_id );

                return true;
            }
        }

        if ( empty( $wishlist_products ) || ! $in_wishlist ) {
            // Add to wishlist.
            Collections::instance()->save_collection_item(
                array(
                    'collection_id' => $collection_id,
                    'product_id'    => $parent_id,
                    'variation_id'  => $variation_id,
                    'quantity'      => $quantity,
                ),
                true
            );
        } else {
            do_action( 'stwlite_before_product_quantity_updated', $parent_id, $collection_id, $quantity, $user_id );

            // Find the product that matches both product_id and variation_id.
            $selected_product = null;
            if ( is_array( $wishlist_products ) ) {
                foreach ( $wishlist_products as $product_item ) {
                    $item_product_id   = isset( $product_item->product_id ) ? (int) $product_item->product_id : 0;
                    $item_variation_id = isset( $product_item->variation_id ) ? (int) $product_item->variation_id : 0;

                    if ( $item_product_id === (int) $parent_id && $item_variation_id === (int) $variation_id ) {
                        $selected_product = $product_item;
                        break;
                    }
                }
                // Fallback to first product if no match found.
                if ( null === $selected_product ) {
                    $selected_product = reset( $wishlist_products );
                }
            } else {
                // Single product object - check if it matches.
                $item_product_id   = isset( $wishlist_products->product_id ) ? (int) $wishlist_products->product_id : 0;
                $item_variation_id = isset( $wishlist_products->variation_id ) ? (int) $wishlist_products->variation_id : 0;

                if ( $item_product_id === (int) $parent_id && $item_variation_id === (int) $variation_id ) {
                    $selected_product = $wishlist_products;
                } else {
                    $selected_product = $wishlist_products;
                }
            }

            $existing_quantity = isset( $selected_product->quantity ) ? (int) $selected_product->quantity : 0;
            $new_quantity      = $existing_quantity + (int) $quantity;
            // Update quantity.
            Collections::instance()->update_collection_quantity_item( $parent_id, $collection_id, $new_quantity, $variation_id );

            do_action( 'stwlite_after_product_quantity_updated', $parent_id, $collection_id, $new_quantity, $existing_quantity, $quantity, $user_id );
        }

        do_action( 'stwlite_after_product_updated', $parent_id, $collection_id, $quantity, $user_id );

        return true;
    }

    /**
     * Check if product is already in wishlist.
     *
     * @param int  $product_id The product ID.
     * @param int  $collection_id The collection ID.
     * @param bool $return_single_product Whether to return a single product.
     * @param bool $exclude_draft Whether to exclude items in draft collections.
     *
     * @since 1.0.0
     * @since 1.0.5 Added $exclude_draft parameter.
     * @since 1.0.7 Shop-loop calls (logged-in user, $collection_id=0, !$return_single_product,
     *              $exclude_draft=true) take a bulk-fetch + in-memory filter path. As a side
     *              effect, the inner filters `stwlite_get_products_in_wishlist_where`,
     *              `stwlite_get_products_in_wishlist_fields`, and `stwlite_get_products_in_wishlist`
     *              fire ONCE with $product_id=0 instead of once per product on that path. The
     *              outer `stwlite_check_if_product_in_wishlist` filter still receives the correct
     *              per-product context.
     * @access public
     *
     * @return array|object|false Products data if found, false otherwise.
     */
    public function check_if_product_in_wishlist( $product_id = 0, $collection_id = 0, $return_single_product = true, $exclude_draft = false ) {
        $user_id = get_current_user_id();

        // Shop and product pages call this once per product card. Replace the
        // per-product JOIN query with a single bulk fetch + in-memory filter so
        // catalog page TTFB stays O(1) in the number of products rendered.
        $is_shop_loop_signature = $product_id > 0
            && 0 === (int) $collection_id
            && ! $return_single_product
            && $exclude_draft;

        if ( 0 === $user_id && $is_shop_loop_signature ) {
            // Guests have no server-side wishlist in Lite (they use localStorage),
            // so the per-product JOIN would always return empty. Short-circuit to
            // keep guest shop pages O(1) — the filter still fires with an empty set.
            $products = array();
        } elseif ( $user_id > 0 && $is_shop_loop_signature ) {
            $all_items = $this->get_user_wishlist_items_cached( $user_id );
            $products  = array();
            foreach ( $all_items as $item ) {
                if ( isset( $item->product_id ) && (int) $item->product_id === (int) $product_id ) {
                    $products[] = $item;
                }
            }
        } else {
            $products = Collections::instance()->get_products_in_wishlist( $product_id, $collection_id, $user_id, array( 'product_id', 'variation_id', 'collection_id', 'quantity' ), true, $return_single_product, $exclude_draft );
        }

        // Filter products.
        $products = apply_filters( 'stwlite_check_if_product_in_wishlist', $products, $product_id, $collection_id );

        return ! empty( $products ) ? $products : false;
    }

    /**
     * Bulk-fetch every non-draft wishlist item for the user, memoized for the
     * duration of the request.
     *
     * Feeds the optimized shop-loop path in check_if_product_in_wishlist() so
     * the per-product DB query collapses into one bulk query plus an in-memory
     * filter, regardless of catalog page size.
     *
     * @param int $user_id The wishlist owner ID.
     *
     * @since 1.0.7
     * @access private
     *
     * @return array List of items with product_id, variation_id, collection_id, quantity.
     */
    private function get_user_wishlist_items_cached( $user_id ) {
        return $this->remember(
            'user_wishlist_items_' . (int) $user_id,
            function () use ( $user_id ) {
                $items = Collections::instance()->get_products_in_wishlist( 0, 0, (int) $user_id, array( 'product_id', 'variation_id', 'collection_id', 'quantity' ), true, false, true );

                // get_products_in_wishlist() returns null on DB error and an array otherwise.
                return (array) $items;
            }
        );
    }

    /**
     * Show public wishlist.
     *
     * @param string $where The where clause.
     *
     * @since 1.0.0
     * @access public
     *
     * @return string The where clause.
     */
    public function show_public_wishlist( $where = '' ) {
        if ( ! is_user_logged_in() ) {
            $where .= ' AND is_public = 1';
        }

        return $where;
    }

    /**
     * Get all wishlists.
     *
     * @param array $params The parameters.
     *
     * @since 1.0.0
     * @access public
     */
    public function get_wishlists( $params ) {
        $collections = Collections::instance()->get_collections( $params );

        // Filter all wishlists.
        $collections = apply_filters( 'stwlite_all_wishlists', $collections, $params );

        return $collections;
    }

    /**
     * Get wishlist options.
     *
     * @since 1.0.0
     * @since 1.0.5 Restrict dropdown to published wishlists for all users.
     * @access public
     *
     * @return array The wishlist options.
     */
    public function get_wishlists_as_options() {
        // The Add to Wishlist dropdown must only show published wishlists,
        // regardless of the current user's role. Draft wishlists are not
        // selectable save targets on the frontend.
        $params = array(
            'status' => 'publish',
        );
        if ( ! current_user_can( 'manage_options' ) ) {
            $params['user_id'] = get_current_user_id();
        }

        // Key the cache by the resolved user scope so admin and customer requests
        // do not share a slot (the admin path drops the user_id filter).
        $cache_key = 'wishlists_as_options_' . ( isset( $params['user_id'] ) ? (int) $params['user_id'] : 'admin' );

        return $this->remember(
            $cache_key,
            function () use ( $params ) {
                $options     = array();
                $collections = $this->get_wishlists( $params );

                if ( ! empty( $collections['data'] ) ) {
                    foreach ( $collections['data'] as $collection ) {
                        $options[] = array(
                            'id'       => $collection->id,
                            'url_code' => $collection->url_code,
                            'name'     => $collection->name,
                        );
                    }
                }

                return apply_filters( 'stwlite_get_wishlist_lists_as_options', $options, $collections );
            }
        );
    }

    /**
     * Get wishlist.
     *
     * @param int $collection_id The collection ID.
     *
     * @since 1.0.0
     * @access public
     *
     * @return object The wishlist data.
     */
    public function get_wishlist( $collection_id ) {
        $collection = Collections::instance()->get_collection( $collection_id );
        $data       = Collection::get_instance( $collection );

        // Filter wishlist data.
        $data = apply_filters( 'stwlite_data', $data, $collection, $collection_id );

        return $data;
    }

    /**
     * Get wishlist items.
     *
     * @param array $params The parameters.
     *
     * @since 1.0.0
     * @access public
     */
    public function get_wishlist_items( $params ) {
        $collection_id  = $params['collection_id'];
        $wishlist_items = Collections::instance()->get_collection_items( $collection_id, $params );

        // Filter wishlist items.
        $wishlist_items = apply_filters( 'stwlite_items', $wishlist_items, $collection_id );

        return $wishlist_items;
    }

    /**
     * Add to cart.
     *
     * @since 1.0.0
     * @access public
     */
    public function add_to_cart() {
        if ( ! check_ajax_referer( 'stwlite_frontend_nonce', 'nonce', false ) ) {
            wp_send_json(
                array(
                    'status'  => 'error',
                    'message' => __( 'Invalid process. Please try again.', 'saveto-wishlist-lite-for-woocommerce' ),
                )
            );
        }

        $collection_id = ! empty( $_POST['collection_id'] ) ? (int) $_POST['collection_id'] : 0;
        $product_id    = ! empty( $_POST['product_id'] ) ? (int) $_POST['product_id'] : 0;
        $quantity      = ! empty( $_POST['quantity'] ) ? (int) $_POST['quantity'] : 0;
        $variation_id  = ! empty( $_POST['variation_id'] ) ? (int) $_POST['variation_id'] : 0;

        do_action( 'stwlite_before_product_added_to_cart', $collection_id, $product_id, $quantity, $variation_id );

        if ( WC()->cart->add_to_cart( $product_id, $quantity, $variation_id ) ) {
            $auto_remove = get_option( 'stwlite_settings_auto_remove_product_from_wishlist', 'yes' );

            if ( 'yes' === $auto_remove ) {
                $variation_ids = array();
                if ( 0 !== (int) $variation_id ) {
                    $variation_ids[] = $variation_id;
                }

                // Remove item from wishlist.
                Collections::instance()->delete_collection_items( $collection_id, array( $product_id ), false, $variation_ids );
            }

            do_action( 'stwlite_after_product_added_to_cart', $collection_id, $product_id, $quantity, $variation_id );

            wp_send_json(
                array(
                    'status'  => 'success',
                    'message' => __( 'Product added to cart successfully.', 'saveto-wishlist-lite-for-woocommerce' ),
                )
            );
        } else {
            wp_send_json(
                array(
                    'status'  => 'error',
                    'message' => __( 'Error adding product to cart.', 'saveto-wishlist-lite-for-woocommerce' ),
                )
            );
        }
    }

    /**
     * Get default wishlist.
     *
     * @param bool $all Whether to return all wishlists.
     *
     * @since 1.0.0
     * @since 1.0.5 Restrict default wishlist to published status for non-admin users.
     * @access public
     *
     * @return array|int The default wishlist data or the default wishlist ID.
     */
    public function get_default_wishlist( $all = false ) {
        $user_id = get_current_user_id();

        // Cache the resolved collection object once per request, then derive the
        // id-vs-object return shape at call time. $all does not change the SQL,
        // so keying on it would cause the same SELECT to run twice when both
        // callers hit this method in a single request.
        $collection = $this->remember(
            'default_wishlist_' . (int) $user_id,
            function () use ( $user_id ) {
                $default_wishlist_params = array(
                    'user_id'      => $user_id,
                    'is_default'   => 1,
                    'user_id_only' => 1,
                );

                // Draft wishlists must not surface on the frontend.
                if ( ! current_user_can( 'manage_options' ) ) {
                    $default_wishlist_params['status'] = 'publish';
                }

                $default_wishlist = Collections::instance()->get_collections( $default_wishlist_params );

                if ( ! empty( $default_wishlist['data'] ) ) {
                    return $default_wishlist['data'][0];
                }

                $fallback = Collections::instance()->get_collections(
                    array(
                        'user_id'      => $user_id,
                        'user_id_only' => 1,
                        'status'       => 'publish',
                    )
                );

                if ( ! empty( $fallback['data'] ) ) {
                    return $fallback['data'][0];
                }

                return null;
            }
        );

        if ( is_object( $collection ) ) {
            $response_wishlist_data = $all ? $collection : $collection->id;
        } else {
            $response_wishlist_data = array();
        }

        return apply_filters( 'stwlite_response_wishlist_data', $response_wishlist_data, $user_id );
    }

    /**
     * Remove product from wishlist.
     *
     * @since 1.0.0
     * @since 1.0.4 Added variation IDs.
     * @access public
     */
    public function remove_product_from_wishlist() {
        if ( ! check_ajax_referer( 'stwlite_frontend_nonce', 'nonce', false ) ) {
            wp_send_json(
                array(
                    'status'  => 'error',
                    'message' => __( 'Invalid process. Please try again.', 'saveto-wishlist-lite-for-woocommerce' ),
                )
            );
        }

        $product_id    = ! empty( $_POST['product_id'] ) ? (int) $_POST['product_id'] : 0;
        $collection_id = ! empty( $_POST['collection_id'] ) ? (int) $_POST['collection_id'] : 0;
        $variation_ids = array();

        do_action( 'stwlite_before_product_removed_from_wishlist', $product_id, $collection_id, $variation_ids );

        // Check if product is variation or simple product.
        $product    = wc_get_product( $product_id );
        $product_id = Helper_Functions::get_parent_product_id( $product, $product_id );
        if ( $product && $product->is_type( 'variation' ) ) {
            $variation_ids[] = $product->get_id();
        }

        // Remove product from wishlist.
        Collections::instance()->delete_collection_items_by_user( array( $product_id ), get_current_user_id(), $variation_ids );

        do_action( 'stwlite_after_product_removed_from_wishlist', $product_id, $collection_id, $variation_ids );

        wp_send_json(
            array(
                'status'  => 'success',
                'message' => __( 'Product removed from wishlist.', 'saveto-wishlist-lite-for-woocommerce' ),
            )
        );
    }

    /**
     * Store wishlist from localStorage to database after account is created/logged in.
     *
     * @since 1.0.0
     * @access public
     */
    public function store_wishlist_from_local_storage() {
        if ( ! check_ajax_referer( 'stwlite_frontend_nonce', 'nonce', false ) ) {
            wp_send_json(
                array(
                    'status'  => 'error',
                    'message' => __( 'Invalid process. Please try again.', 'saveto-wishlist-lite-for-woocommerce' ),
                )
            );
        }

        $items_added    = array();
        $wishlist_items = ! empty( $_POST['wishlist'] ) ? wp_unslash( $_POST['wishlist'] ) : array(); // phpcs:ignore

        if ( ! empty( $wishlist_items ) ) {
            // Check if logged in user has a wishlist.
            $user_id  = get_current_user_id();
            $wishlist = Collections::instance()->get_collections(
                array(
                    'user_id'      => $user_id,
                    'user_id_only' => 1,
                )
            );

            do_action( 'stwlite_before_store_wishlist_from_local_storage', $wishlist_items, $wishlist, $user_id );

            $wishlist_id = 0;
            if ( ! empty( $wishlist['data'] ) ) {
                $wishlist_data = array();
                foreach ( $wishlist['data'] as $wishlist_item ) {
                    if ( 1 === (int) $wishlist_item->is_default ) {
                        $wishlist_id = $wishlist_item->id;
                        break;
                    } else {
                        $wishlist_data[] = $wishlist_item->id;
                    }
                }

                if ( ! empty( $wishlist_data ) && 0 === $wishlist_id ) {
                    // Update default wishlist.
                    Collections::instance()->update_default_wishlist( $user_id );

                    $first_wishlist = $wishlist_data[0];
                    $wishlist_id    = $first_wishlist->id;

                    $params = array(
                        'id'          => $wishlist_id,
                        'user_id'     => $first_wishlist->user_id,
                        'name'        => $first_wishlist->name,
                        'status'      => 'publish',
                        'is_default'  => 1,
                        'description' => $first_wishlist->description,
                    );
                    Collections::instance()->save_collection( $params );
                }
            } else {
                // Create a new wishlist.
                $new_wishlist = Collections::instance()->save_collection(
                    array(
                        'user_id'    => $user_id,
                        'name'       => $this->helper_functions->get_default_wishlist_label(),
                        'status'     => 'publish',
                        'is_default' => 1,
                        'is_public'  => 0,
                    )
                );

                $wishlist_id = $new_wishlist->id;
            }

            do_action( 'stwlite_after_store_wishlist_from_local_storage', $wishlist_items, $wishlist, $user_id );

            foreach ( $wishlist_items as $wishlist_item ) {
                $item_id = Collections::instance()->save_collection_item(
                    array(
                        'collection_id' => $wishlist_id,
                        'product_id'    => $wishlist_item['productId'],
                        'quantity'      => $wishlist_item['quantity'],
                    )
                );

                $items_added[] = $wishlist_item['productId'];
            }
        }

        wp_send_json(
            array(
                'status'  => 'success',
                'message' => __( 'Wishlist stored from localStorage to database.', 'saveto-wishlist-lite-for-woocommerce' ),
                'items'   => $items_added,
            )
        );
    }

    /**
     * Auto create wishlist if no wishlist exists.
     *
     * @param array $wishlist_data The wishlist data.
     * @param int   $user_id The user ID.
     *
     * @since 1.0.0
     * @access public
     *
     * @return array The wishlist data.
     */
    public function auto_create_wishlist( $wishlist_data, $user_id ) {
        if ( empty( $wishlist_data ) && 0 !== $user_id ) {
            $wishlist_data = Collections::instance()->save_collection(
                array(
                    'user_id'     => $user_id,
                    'name'        => $this->helper_functions->get_default_wishlist_label(),
                    'description' => '',
                    'status'      => 'publish',
                    'is_default'  => 1,
                    'is_public'   => 0,
                )
            );
        }

        return $wishlist_data;
    }

    /**
     * Get variation ID from attributes.
     *
     * @param int $product_id The product ID.
     *
     * @since 1.0.4 Added variation IDs.
     * @access public
     *
     * @return int The variation ID.
     */
    public function get_variation_id_from_attributes( $product_id ) {
        $variation_id = 0;
        if ( is_product() ) {
            $product = wc_get_product( $product_id );
            if ( $product && $product->is_type( 'variable' ) ) {

                // phpcs:disable

                // Check if variation_id is directly available in POST/GET.
                if ( ! empty( $_POST['variation_id'] ) ) {
                    $variation_id = (int) $_POST['variation_id'];
                } elseif ( ! empty( $_GET['variation_id'] ) ) {
                    $variation_id = (int) $_GET['variation_id'];
                } else {
                    // Check if variation attributes are selected in the form.
                    $selected_attributes  = array();
                    $variation_attributes = $product->get_variation_attributes();

                    foreach ( $variation_attributes as $attribute_name => $options ) {
                        $attribute_key = 'attribute_' . sanitize_title( $attribute_name );
                        if ( isset( $_POST[ $attribute_key ] ) && ! empty( $_POST[ $attribute_key ] ) ) {
                            $selected_attributes[ $attribute_name ] = sanitize_text_field( wp_unslash( $_POST[ $attribute_key ] ) );
                        } elseif ( isset( $_GET[ $attribute_key ] ) && ! empty( $_GET[ $attribute_key ] ) ) {
                            $selected_attributes[ $attribute_name ] = sanitize_text_field( wp_unslash( $_GET[ $attribute_key ] ) );
                        }
                    }

                    // If we have selected attributes, find the matching variation ID.
                    if ( ! empty( $selected_attributes ) && count( $selected_attributes ) === count( $variation_attributes ) ) {
                        // Get all variation IDs for this product.
                        $variation_ids = $product->get_children();

                        // Normalize selected attributes to match variation attribute format.
                        $normalized_attributes = array();
                        foreach ( $selected_attributes as $attribute_name => $attribute_value ) {
                            $normalized_attributes[ sanitize_title( $attribute_name ) ] = $attribute_value;
                        }
                        
                        // Sort normalized attributes for consistent comparison.
                        ksort( $normalized_attributes );

                        // Loop through variations to find a match.
                        foreach ( $variation_ids as $vid ) {
                            $variation = wc_get_product( $vid );
                            if ( ! $variation || ! $variation->exists() ) {
                                continue;
                            }

                            $variation_attrs = $variation->get_attributes();
                            
                            // Sort variation attributes for consistent comparison.
                            ksort( $variation_attrs );

                            // Use array comparison instead of loop - much faster.
                            if ( $normalized_attributes === $variation_attrs ) {
                                $variation_id = $vid;
                                break;
                            }
                        }
                    }
                }

                // phpcs:enable
            }
        }

        return $variation_id;
    }

    /**
     * Check if product is in wishlist.
     *
     * @since 1.0.4 Added variation IDs.
     * @access public
     */
    public function check_product_in_wishlist() {
        if ( ! check_ajax_referer( 'stwlite_frontend_nonce', 'nonce', false ) ) {
            wp_send_json(
                array(
                    'status'  => 'error',
                    'message' => __( 'Invalid process. Please try again.', 'saveto-wishlist-lite-for-woocommerce' ),
                )
            );
        }

        $product_id   = ! empty( $_POST['product_id'] ) ? (int) $_POST['product_id'] : 0;
        $variation_id = ! empty( $_POST['variation_id'] ) ? (int) $_POST['variation_id'] : 0;

        $product = wc_get_product( $product_id );
        if ( ! $product ) {
            wp_send_json(
                array(
                    'status'  => 'error',
                    'message' => __( 'Invalid product.', 'saveto-wishlist-lite-for-woocommerce' ),
                )
            );
        }

        $selected_product  = $this->check_if_product_in_wishlist( $product_id, 0, false, true );
        $selected_wishlist = $this->product_check_in_wishlist( $product, $selected_product, $variation_id, is_user_logged_in() );

        wp_send_json(
            array(
                'status' => 'success',
                'exists' => $selected_wishlist,
            )
        );
    }

    /**
     * Check if product is in wishlist.
     *
     * @param object $product The product object.
     * @param array  $selected_product The selected product.
     * @param int    $current_variation_id The current variation ID.
     * @param bool   $is_user_logged_in Whether the user is logged in.
     *
     * @since 1.0.4 Added variation IDs.
     * @access public
     */
    public function product_check_in_wishlist( $product, $selected_product, $current_variation_id, $is_user_logged_in = true ) {
        $selected_wishlist = false;

        // Validate if product is valid object.
        if ( ! $product ) {
            return $selected_wishlist;
        }

        if ( ! empty( $selected_product ) && $is_user_logged_in && $product->is_type( 'simple' ) ) {
            $selected_wishlist = true;
        } elseif ( 0 === (int) $current_variation_id && $is_user_logged_in && ! empty( $selected_product ) ) {
            // Handle array of products.
            if ( is_array( $selected_product ) ) {
                foreach ( $selected_product as $product_item ) {
                    if ( isset( $product_item->variation_id ) && 0 === (int) $product_item->variation_id ) {
                        $selected_wishlist = true;
                        break;
                    }
                }
            }
        } elseif ( (int) $current_variation_id > 0 && $is_user_logged_in ) { // If variation ID is not empty, add it to the product ID.
            $selected_wishlist = false;

            // Check if current variation ID exists in the selected product data.
            if ( ! empty( $selected_product ) ) {
                // Handle array of products.
                if ( is_array( $selected_product ) ) {
                    foreach ( $selected_product as $product_item ) {
                        if ( isset( $product_item->variation_id ) && (int) $current_variation_id === (int) $product_item->variation_id ) {
                            $selected_wishlist = true;
                            break;
                        }
                    }
                } elseif ( is_object( $selected_product ) && isset( $selected_product->variation_id ) && (int) $current_variation_id === (int) $selected_product->variation_id ) {
                    // Handle single product object.
                    $selected_wishlist = true;
                }
            }
        }

        return $selected_wishlist;
    }
}
