<?php
/**
 * Facebook Pixel Plugin FacebookWordpressWooCommerce class.
 *
 * This file contains the main logic for FacebookWordpressWooCommerce.
 *
 * @package FacebookPixelPlugin
 */

/**
 * Define FacebookWordpressWooCommerce class.
 *
 * @return void
 */

/*
* Copyright (C) 2017-present, Meta, Inc.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; version 2 of the License.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*/

namespace FacebookPixelPlugin\Integration;

defined( 'ABSPATH' ) || die( 'Direct access not allowed' );

use FacebookPixelPlugin\Core\FacebookPixel;
use FacebookPixelPlugin\Core\FacebookPluginUtils;
use FacebookPixelPlugin\Core\ServerEventFactory;
use FacebookPixelPlugin\Core\FacebookServerSideEvent;
use FacebookPixelPlugin\Core\PixelRenderer;
use FacebookPixelPlugin\FacebookAds\Object\ServerSide\Content;

/**
 * FacebookWordpressWooCommerce class.
 */
class FacebookWordpressWooCommerce extends FacebookWordpressIntegrationBase {
    const PLUGIN_FILE   =
    'facebook-for-woocommerce/facebook-for-woocommerce.php';
    const TRACKING_NAME = 'woocommerce';

    const FB_ID_PREFIX = 'wc_post_id_';

    const DIV_ID_FOR_AJAX_PIXEL_EVENTS = 'fb-pxl-ajax-code';

    /**
     * Injects Facebook Pixel events for WooCommerce.
     *
     * This method sets up WordPress actions to inject Facebook Pixel events
     * for different stages of the WooCommerce process, only if the
     * Facebook for WooCommerce plugin is not active.
     *
     * - InitiateCheckout: Injects pixel event after checkout form.
     * - AddToCart: Tracks add to cart actions.
     * - Purchase: Fires pixel event on purchase completion.
     * - ViewContent: Tracks product page views.
     * - AJAX: Adds a footer div for AJAX-triggered events.
     *
     * Hooks are added with a priority of 40, and the add to cart event
     * includes four parameters.
     *
     * @return void
     */
    public static function inject_pixel_code() {
        if ( ! self::isFacebookForWooCommerceActive() ) {
            // InitiateCheckout events for classic checkout.
            add_action(
                'woocommerce_after_checkout_form',
                array( __CLASS__, 'trackInitiateCheckout' ),
                40
            );
            // InitiateCheckout events for block-based checkout.
            add_action(
                'woocommerce_blocks_checkout_enqueue_data',
                array( __CLASS__, 'trackInitiateCheckout' )
            );

            add_action(
                'woocommerce_add_to_cart',
                array( __CLASS__, 'trackAddToCartEvent' ),
                40,
                4
            );

            add_action(
                'woocommerce_thankyou',
                array( __CLASS__, 'trackPurchaseEvent' ),
                40
            );

            add_action(
                'woocommerce_payment_complete',
                array( __CLASS__, 'trackPurchaseEvent' ),
                40
            );

            add_action(
                'woocommerce_after_single_product',
                array( __CLASS__, 'trackViewContentEvent' ),
                40
            );

            add_action(
                'wp_footer',
                array( __CLASS__, 'addDivForAjaxPixelEvent' )
            );
        }
    }

    /**
     * Injects a hidden div with an id of
     * 'fb-pxl-ajax-code' into the page footer.
     * This div is used to inject pixel events via AJAX requests.
     */
    public static function addDivForAjaxPixelEvent() {
        echo wp_kses(
            self::getDivForAjaxPixelEvent(),
            array(
                'div' => array(
                    'id' => array(),
                ),
            )
        );
    }

    /**
     * Generates a div element with a specific ID for
     * AJAX-triggered pixel events.
     *
     * This method returns a div element with the ID defined
     * by DIV_ID_FOR_AJAX_PIXEL_EVENTS.
     * The function allows for optional content to be included inside the div.
     *
     * @param string $content Optional content to be placed inside the div.
     * @return string HTML div element as a string.
     */
    public static function getDivForAjaxPixelEvent( $content = '' ) {
        return "<div id='" . self::DIV_ID_FOR_AJAX_PIXEL_EVENTS . "'>"
        . $content . '</div>';
    }

    /**
     * Injects a ViewContent event into the page, but only if the user is not an
     * internal user (i.e. an admin user).
     *
     * The event is only injected if a valid product
     * object can be retrieved from
     * the current post ID. If not, the method simply exits.
     *
     * The ViewContent event is generated by calling
     * the createViewContentEvent method
     * and passing in the product object as an argument.
     * The event is then tracked
     * using the FacebookServerSideEvent singleton,
     * and the pixel code is enqueued
     * for output.
     *
     * @return void
     */
    public static function trackViewContentEvent() {
        if ( FacebookPluginUtils::is_internal_user() ) {
            return;
        }

        global $post;
        if ( ! isset( $post->ID ) ) {
            return;
        }

        $product = wc_get_product( $post->ID );
        if ( ! $product ) {
            return;
        }

        $server_event = ServerEventFactory::safe_create_event(
            'ViewContent',
            array( __CLASS__, 'createViewContentEvent' ),
            array( $product ),
            self::TRACKING_NAME
        );

        FacebookServerSideEvent::get_instance()->track( $server_event, false );

        self::enqueuePixelCode( $server_event );
    }

    /**
     * Generates a ViewContent event data.
     *
     * The ViewContent event is generated by
     * setting fields such as content_type,
     * currency, value, content_ids, content_name and content_category.
     *
     * @param WC_Product $product Product object.
     * @return array The event data.
     */
    public static function createViewContentEvent( $product ) {
        $event_data = self::getPIIFromSession();

        $product_id   = self::getProductId( $product );
        $content_type = $product->is_type( 'variable' ) ?
        'product_group' : 'product';

        $event_data['content_type']     = $content_type;
        $event_data['currency']         = \get_woocommerce_currency();
        $event_data['value']            = $product->get_price();
        $event_data['content_ids']      = array( $product_id );
        $event_data['content_name']     = $product->get_title();
        $event_data['content_category'] = self::getProductCategory(
            $product->get_id()
        );

        return array_filter( $event_data );
    }

    /**
     * Returns the first category name of a given product ID.
     *
     * This method gets all the categories associated with the given product ID
     * and returns the first category name. If no categories are associated with
     * the product, the method returns null.
     *
     * @param int $product_id Product ID.
     * @return string|null First category name
     * associated with the product, or null.
     */
    private static function getProductCategory( $product_id ) {
        $categories = get_the_terms(
            $product_id,
            'product_cat'
        );
        return ! empty( $categories ) && count( $categories ) > 0 ? $categories[0]->name : null;
    }

    /**
     * Tracks a Meta Pixel Purchase event.
     *
     * This method is a callback for the `woocommerce_thankyou` action hook.
     * It tracks a Meta Pixel Purchase event whenever a purchase is completed.
     *
     * @param int $order_id The order ID.
     *
     * @since 1.0.0
     */
    public static function trackPurchaseEvent( $order_id ) {
        if ( FacebookPluginUtils::is_internal_user() ) {
            return;
        }

        $server_event = ServerEventFactory::safe_create_event(
            'Purchase',
            array( __CLASS__, 'createPurchaseEvent' ),
            array( $order_id ),
            self::TRACKING_NAME
        );

        FacebookServerSideEvent::get_instance()->track( $server_event );

        self::enqueuePixelCode( $server_event );
    }

    /**
     * Generates a Meta Pixel Purchase event data.
     *
     * The Purchase event is fired when a customer completes a purchase.
     * It is typically sent when a customer submits an order.
     *
     * The method loops through the items in the order and creates a
     * Meta Pixel Content object for each item. The method then sets the
     * content_type, currency, value, content_ids and contents fields in
     * the event data.
     *
     * @param int $order_id The order ID.
     *
     * @return array The event data.
     *
     * @since 1.0.0
     */
    public static function createPurchaseEvent( $order_id ) {
        $order = wc_get_order( $order_id );

        $content_type = 'product';
        $product_ids  = array();
        $contents     = array();

        foreach ( $order->get_items() as $item ) {
            $product = wc_get_product( $item->get_product_id() );
            if ( ! is_object( $product ) || ! method_exists( $product, 'get_id' ) ) {
                continue;
            }

            if ( 'product_group' !== $content_type
            && $product->is_type( 'variable' ) ) {
            $content_type = 'product_group';
            }

            $quantity   = $item->get_quantity();
            $product_id = self::getProductId( $product );

            $content = new Content();
            $content->setProductId( $product_id );
            $content->setQuantity( $quantity );
            $content->setItemPrice( $item->get_total() / $quantity );

            $contents[]    = $content;
            $product_ids[] = $product_id;
        }

        $event_data                 = self::getPiiFromBillingInformation(
            $order
        );
        $event_data['content_type'] = $content_type;
        $event_data['currency']     = \get_woocommerce_currency();
        $event_data['value']        = $order->get_total();
        $event_data['content_ids']  = $product_ids;
        $event_data['contents']     = $contents;

        return $event_data;
    }

    /**
     * Generates a Meta Pixel AddToCart event data.
     *
     * The AddToCart event is fired when a customer
     * adds a product to their cart.
     * It is typically sent when a customer submits a
     * form to add a product to their cart.
     *
     * The method loops through the items in the cart and creates a
     * Meta Pixel Content object for each item. The method then sets the
     * content_type, currency, value, content_ids and contents fields in
     * the event data.
     *
     * @param string $cart_item_key The cart item key.
     * @param int    $product_id    The product ID.
     * @param int    $quantity      The quantity of the item in the cart.
     * @param int    $variation_id  The variation ID.
     *
     * @since 1.0.0
     */
    public static function trackAddToCartEvent(
        $cart_item_key,
        $product_id,
        $quantity,
        $variation_id
    ) {
        if ( FacebookPluginUtils::is_internal_user() ) {
            return;
        }

        $server_event = ServerEventFactory::safe_create_event(
            'AddToCart',
            array( __CLASS__, 'createAddToCartEvent' ),
            array( $cart_item_key, $product_id, $quantity, $variation_id ),
            self::TRACKING_NAME
        );

            $is_ajax_request = wp_doing_ajax();

        FacebookServerSideEvent::get_instance()->track(
            $server_event,
            $is_ajax_request
        );

        if ( ! $is_ajax_request ) {
            self::enqueuePixelCode( $server_event );
        } else {
            FacebookServerSideEvent::get_instance()->set_pending_pixel_event(
                'addPixelCodeToAddToCartFragment',
                $server_event
            );
            add_filter(
                'woocommerce_add_to_cart_fragments',
                array( __CLASS__, 'addPixelCodeToAddToCartFragment' )
            );
        }
    }

    /**
     * Modifies the WooCommerce "add to cart" AJAX fragment response
     * to include the Meta Pixel code.
     *
     * This method is used to inject the Meta Pixel code into the
     * page after the user has added a product to their cart.
     *
     * @param array $fragments The response array passed to the
     *                          "woocommerce_add_to_cart_fragments" filter.
     *
     * @return array The modified response array.
     *
     * @since 1.0.0
     */
    public static function addPixelCodeToAddToCartFragment( $fragments ) {
        $server_event = FacebookServerSideEvent::get_instance()
        ->get_pending_pixel_event( 'addPixelCodeToAddToCartFragment' );
        if ( ! is_null( $server_event ) ) {
            $pixel_code = self::generatePixelCode( $server_event, true );
            $fragments[ '#' . self::DIV_ID_FOR_AJAX_PIXEL_EVENTS ] =
            self::getDivForAjaxPixelEvent( $pixel_code );
        }
        return $fragments;
    }

    /**
     * Creates a Meta Pixel AddToCart event data.
     *
     * The AddToCart event is fired when a customer adds a product to their
     * cart. It is typically sent when a customer adds a product to their
     * cart.
     *
     * @param string $cart_item_key The cart item key.
     * @param int    $product_id    The product ID.
     * @param int    $quantity      The quantity.
     * @param int    $variation_id  The variation ID.
     *
     * @return array The event data.
     *
     * @since 1.0.0
     */
    public static function createAddToCartEvent(
        $cart_item_key,
        $product_id,
        $quantity,
        $variation_id = null
    ) {
        $event_data                 = self::getPIIFromSession();
        $event_data['content_type'] = 'product';
        $event_data['currency']     = \get_woocommerce_currency();

        $cart_item = self::getCartItem( $cart_item_key );
        if ( ! empty( $cart_item ) && ! empty( $cart_item['data'] ) ) {
            $event_data['content_ids'] = array(
                self::getProductId(
                    $cart_item['data']
                ),
            );
            $event_data['value']       = self::getAddToCartValue(
                $cart_item,
                $quantity
            );

            return $event_data;
        }

        // Fallback for integrations that call Woo add_to_cart() on a
        // temporary/private cart (e.g. subscription cloning flows).
        $product_lookup_id = ! empty( $variation_id ) ? $variation_id : $product_id;
        $product           = wc_get_product( $product_lookup_id );
        $product_fb_id     = self::getProductId( $product );

        if ( ! empty( $product_fb_id ) ) {
            $event_data['content_ids'] = array( $product_fb_id );

            if ( is_object( $product ) && method_exists( $product, 'get_price' ) ) {
                $event_data['value'] = (float) $quantity * (float) $product->get_price();
            }
        }

        return $event_data;
    }

    /**
     * Tracks a Meta Pixel InitiateCheckout event.
     *
     * This method is a wrapper of FacebookServerSideEvent::track() method.
     * It creates a Meta Pixel InitiateCheckout event data with the data
     * from the WooCommerce session, and then tracks the event.
     *
     * @since 1.0.0
     */
    public static function trackInitiateCheckout() {
        if ( FacebookPluginUtils::is_internal_user() || null === WC()->cart || WC()->cart->get_cart_contents_count() === 0 ) {
            return;
        }

        $server_event = ServerEventFactory::safe_create_event(
            'InitiateCheckout',
            array( __CLASS__, 'createInitiateCheckoutEvent' ),
            array(),
            self::TRACKING_NAME
        );

        FacebookServerSideEvent::get_instance()->track( $server_event );

        self::enqueuePixelCode( $server_event );
    }

    /**
     * Creates a Meta Pixel InitiateCheckout event data.
     *
     * The InitiateCheckout event is triggered when
     * a customer initiates the checkout process.
     * This method gathers personal identifiable
     * information (PII) from the session and
     * retrieves cart details such as the number
     * of items, total value, content IDs,
     * and contents of the cart. The event data
     * is then returned for tracking purposes.
     *
     * @return array The event data including
     * user PII, cart details, and currency information.
     *
     * @since 1.0.0
     */
    public static function createInitiateCheckoutEvent() {
        $event_data                 = self::getPIIFromSession();
        $event_data['content_type'] = 'product';
        $event_data['currency']     = \get_woocommerce_currency();

        if ( WC()->cart ) {
            $cart = WC()->cart;

            $event_data['num_items']   = $cart->get_cart_contents_count();
            $event_data['value']       = $cart->total;
            $event_data['content_ids'] = self::getContentIds( $cart );
            $event_data['contents']    = self::getContents( $cart );
        }

        return $event_data;
    }

    /**
     * Retrieves personally identifiable
     * information (PII) from a WooCommerce order.
     *
     * This method extracts billing details
     * from the given WooCommerce order object,
     * including the first name, last name,
     * email, postal code, state, country, city,
     * and phone number. The PII is
     * returned as an associative array.
     *
     * @param WC_Order $order The WooCommerce
     * order object containing billing information.
     *
     * @return array An associative array containing the extracted PII.
     *
     * @since 1.0.0
     */
    private static function getPiiFromBillingInformation( $order ) {
        $pii = array();

        $pii['first_name'] = $order->get_billing_first_name();
        $pii['last_name']  = $order->get_billing_last_name();
        $pii['email']      = $order->get_billing_email();
        $pii['zip']        = $order->get_billing_postcode();
        $pii['state']      = $order->get_billing_state();
        $pii['country']    = $order->get_billing_country();
        $pii['city']       = $order->get_billing_city();
        $pii['phone']      = $order->get_billing_phone();

        return $pii;
    }

    /**
     * Calculates the total value for adding a specified
     * quantity of a cart item to the cart.
     *
     * This method computes the total cost based on the
     * line total and quantity of the provided
     * cart item, and multiplies it by the specified quantity.
     *
     * @param array $cart_item An associative array
     *                          representing the cart item, containing
     *                         'line_total' and 'quantity' keys among others.
     * @param int   $quantity  The quantity of
     *              the item to calculate the total value for.
     *
     * @return float|null The calculated total value for
     *                    the specified quantity of the item,
     *                    or null if the cart item is empty.
     */
    private static function getAddToCartValue( $cart_item, $quantity ) {
        if ( ! empty( $cart_item ) ) {
            $price = $cart_item['line_total'] / $cart_item['quantity'];
            return $quantity * $price;
        }

        return null;
    }

    /**
     * Retrieves a cart item from the WooCommerce cart by its key.
     *
     * This method accesses the current WooCommerce cart and returns the
     * cart item associated with the provided cart item key. If the cart
     * or the specified cart item is not found, the method returns null.
     *
     * @param string $cart_item_key The key for identifying the cart item.
     *
     * @return array|null An associative array representing the cart item,
     *                    or null if the cart item is not found.
     */
    private static function getCartItem( $cart_item_key ) {
        if ( WC()->cart ) {
            $cart = WC()->cart->get_cart();
            if ( ! empty( $cart ) && ! empty( $cart[ $cart_item_key ] ) ) {
            return $cart[ $cart_item_key ];
            }
        }

        return null;
    }

    /**
     * Retrieves an array of product IDs from the given WooCommerce cart object.
     *
     * @param WC_Cart $cart The WooCommerce cart object.
     *
     * @return array An array of product IDs.
     *
     * @since 1.0.0
     */
    private static function getContentIds( $cart ) {
        $product_ids = array();
        foreach ( $cart->get_cart() as $item ) {
            if ( ! empty( $item['data'] ) ) {
            $product_ids[] = self::getProductId( $item['data'] );
            }
        }

        return $product_ids;
    }

    /**
     * Retrieves an array of Content objects from
     * the given WooCommerce cart object.
     *
     * Each Content object represents an item in the cart and includes
     * the product ID, quantity, and item price.
     *
     * @param WC_Cart $cart The WooCommerce cart object.
     *
     * @return Content[] An array of Content
     * objects representing the items in the cart.
     *
     * @since 1.0.0
     */
    private static function getContents( $cart ) {
        $contents = array();
        foreach ( $cart->get_cart() as $item ) {
            if ( ! empty( $item['data'] ) && ! empty( $item['quantity'] ) ) {
            $content = new Content();
            $content->setProductId( self::getProductId( $item['data'] ) );
            $content->setQuantity( $item['quantity'] );
            $content->setItemPrice( $item['line_total'] / $item['quantity'] );

            $contents[] = $content;
            }
        }

        return $contents;
    }

    /**
     * Retrieves a unique product ID from the given WooCommerce product object.
     *
     * If the product has a SKU, the ID is in the format of "sku_woo_id".
     * Otherwise, the ID is in the format of
     * "fb_woo_id" where "fb_" is a prefix.
     *
     * @param WC_Product $product The WooCommerce product object.
     *
     * @return string The unique product ID.
     *
     * @since 1.0.0
     */
    private static function getProductId( $product ) {
        if ( ! is_object( $product ) || ! method_exists( $product, 'get_id' ) ) {
            return null;
        }

        $woo_id = $product->get_id();

        return $product->get_sku() ? $product->get_sku() . '_' .
        $woo_id : self::FB_ID_PREFIX . $woo_id;
    }

    /**
     * Retrieves PII from the logged in user's session.
     *
     * @return array The user's PII data.
     *
     * @since 1.0.0
     */
    private static function getPIIFromSession() {
        $event_data = FacebookPluginUtils::get_logged_in_user_info();
        $user_id    = get_current_user_id();
        if ( 0 !== $user_id ) {
            $event_data['city']    = get_user_meta(
                $user_id,
                'billing_city',
                true
            );
            $event_data['zip']     = get_user_meta(
                $user_id,
                'billing_postcode',
                true
            );
            $event_data['country'] = get_user_meta(
                $user_id,
                'billing_country',
                true
            );
            $event_data['state']   = get_user_meta(
                $user_id,
                'billing_state',
                true
            );
            $event_data['phone']   = get_user_meta(
                $user_id,
                'billing_phone',
                true
            );
        }
        return array_filter( $event_data );
    }

    /**
     * Checks if Facebook for WooCommerce plugin is active.
     *
     * @return bool True if Facebook for WooCommerce is active, false otherwise.
     *
     * @since 1.0.0
     */
    private static function isFacebookForWooCommerceActive() {
        return in_array(
            'facebook-for-woocommerce/facebook-for-woocommerce.php',
            get_option( 'active_plugins' ),
            true
        );
    }

    /**
     * Generates the pixel code for a given server event.
     *
     * @param FacebookServerSideEvent $server_event The server event
     * to generate the pixel code for.
     * @param bool                    $script_tag   Whether to wrap
     * the pixel code in a script tag. Default to false.
     *
     * @return string The pixel code for the given server event.
     *
     * @since 1.0.0
     */
    public static function generatePixelCode(
        $server_event,
        $script_tag = false
) {
        $code = PixelRenderer::render(
            array( $server_event ),
            self::TRACKING_NAME,
            $script_tag
        );
        $code = sprintf(
            '
    <!-- Meta Pixel Event Code -->
    %s
    <!-- End Meta Pixel Event Code -->
          ',
            $code
        );
        return $code;
    }

    /**
     * Enqueues a Meta Pixel event code for a given server event.
     *
     * This method renders the Meta Pixel code for the given server event,
     * and then enqueues it using the WooCommerce JavaScript enqueueing
     * system.
     *
     * @param FacebookServerSideEvent $server_event The server
     * event to enqueue the pixel code for.
     *
     * @return string The Meta Pixel code for the given server event.
     *
     * @since 1.0.0
     */
    public static function enqueuePixelCode( $server_event ) {
        $code   = self::generatePixelCode( $server_event, false );
        $handle = 'meta_pixel_inline';

        if ( ! function_exists( 'wp_add_inline_script' ) ) {
            global $wc_queued_js;
            $wc_queued_js .= "\n" . $code;
            return $code;
        }

        static $registered = false;
        if ( ! $registered ) {
            if ( ! wp_script_is( $handle, 'registered' ) ) {
                wp_register_script(
                    $handle,
                    '',
                    array(),
                    \FacebookPixelPlugin\Core\FacebookPluginConfig::PLUGIN_VERSION,
                    true
                );
            }
            wp_enqueue_script( $handle );
            $registered = true;
        }

        wp_add_inline_script( $handle, $code );

        return $code;
    }
}
