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

/**
 * Define FacebookWordpressWPForms 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\FacebookServerSideEvent;
use FacebookPixelPlugin\Core\FacebookWordPressOptions;
use FacebookPixelPlugin\Core\ServerEventFactory;
use FacebookPixelPlugin\Core\PixelRenderer;
use FacebookPixelPlugin\FacebookAds\Object\ServerSide\Event;
use FacebookPixelPlugin\FacebookAds\Object\ServerSide\UserData;

/**
 * FacebookWordpressWPForms class.
 */
class FacebookWordpressWPForms extends FacebookWordpressIntegrationBase {
    const PLUGIN_FILE   = 'wpforms-lite/wpforms.php';
    const TRACKING_NAME = 'wpforms-lite';

    /**
     * Hooks into WPForms to inject the Pixel code.
     *
     * This method adds an action to the 'wpforms_process_before' hook,
     * which will trigger the 'trackEvent' method. It ensures that
     * the Pixel code is injected during the form processing stage.
     */
    public static function inject_pixel_code() {
        // Tracks server and browser events when a submission is processed.
        add_action(
            'wpforms_process_before',
            array( __CLASS__, 'trackEvent' ),
            20,
            2
        );

        // Enriches AJAX responses (success or redirect) with pixel code.
        add_filter(
            'wpforms_ajax_submit_success_response',
            array( __CLASS__, 'injectLeadEventAjax' ),
            20,
            3
        );
        add_filter(
            'wpforms_ajax_submit_redirect',
            array( __CLASS__, 'injectLeadEventAjax' ),
            20,
            3
        );

        // Adds a front-end listener that fires pixel code returned in AJAX responses.
        add_action(
            'wp_footer',
            array( __CLASS__, 'injectAjaxListener' ),
            9
        );
    }

    /**
     * Tracks a server-side event for a form submission in WPForms.
     *
     * This method is hooked into the 'wpforms_process_before' action, which is
     * fired by WPForms before a form is processed.
     * It then calls the track method
     * on the FacebookServerSideEvent instance, which generates a lead event for
     * the form submission.
     *
     * If the user is an internal user, the method returns without tracking
     * any event.
     *
     * @param array $entry The form entry data.
     * @param array $form_data The form data.
     *
     * @return void
     */
    public static function trackEvent( $entry, $form_data ) {
        if ( FacebookPluginUtils::is_internal_user() ) {
            return;
        }

        $server_event = ServerEventFactory::safe_create_event(
            'Lead',
            array( __CLASS__, 'readFormData' ),
            array( $entry, $form_data ),
            self::TRACKING_NAME,
            true
        );
        FacebookServerSideEvent::get_instance()->track( $server_event );

        add_action(
            'wp_footer',
            array( __CLASS__, 'injectLeadEvent' ),
            20
        );
    }

    /**
     * Injects lead event code into the footer.
     *
     * This method retrieves tracked events from the FacebookServerSideEvent
     * instance and renders them into pixel code using the PixelRenderer.
     * The resulting code is printed into the footer section of the page.
     * If the user is an internal user, the method returns without injecting
     * any code.
     *
     * @return void
     */
    public static function injectLeadEvent() {
        if ( FacebookPluginUtils::is_internal_user() ) {
            return;
        }

        $events     =
            FacebookServerSideEvent::get_instance()->get_tracked_events();
        $pixel_code = PixelRenderer::render( $events, self::TRACKING_NAME );

        printf(
            '
    <!-- Meta Pixel Event Code -->
    %s
    <!-- End Meta Pixel Event Code -->
          ',
            $pixel_code // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
        );
    }

    /**
     * Add pixel code into AJAX success/redirect responses.
     *
     * @param array $response  Existing AJAX response payload.
     * @param int   $form_id   Form ID (provided by WPForms).
     * @param mixed $extra     Unused extra parameter (URL or form data).
     *
     * @return array Modified response containing fb_pxl_code when available.
     */
    public static function injectLeadEventAjax( $response, $form_id = null, $extra = null ) {
        if ( FacebookPluginUtils::is_internal_user() ) {
            return $response;
        }

        $events = FacebookServerSideEvent::get_instance()->get_tracked_events();
        if ( empty( $events ) ) {
            return $response;
        }

        $response['fb_pxl_code'] = PixelRenderer::render(
            $events,
            self::TRACKING_NAME,
            false // Return raw fbq calls; they will be eval'd on the client.
        );

        return $response;
    }

    /**
     * Outputs a JS listener that evaluates fb_pxl_code from WPForms AJAX responses.
     *
     * This covers the default WPForms AJAX path where the page is not reloaded
     * and no wp_footer hook is executed after submission.
     *
     * @return void
     */
    public static function injectAjaxListener() {
        ?>
        <!-- Meta Pixel Event Code -->
        <script type='text/javascript'>
        (function ( $ ) {
            if ( ! $ || typeof document === 'undefined' ) {
                return;
            }
            // WPForms triggers this jQuery event and passes the AJAX response object.
            $( document ).on( 'wpformsAjaxSubmitSuccess', function ( event, data ) {
                if ( data && data.data && data.data.fb_pxl_code ) {
                    try {
                        new Function( data.data.fb_pxl_code )();
                    } catch ( e ) {
                        console && console.warn && console.warn( 'Meta Pixel eval failed', e );
                    }
                }
            } );
        })( window.jQuery );
        </script>
        <!-- End Meta Pixel Event Code -->
        <?php
    }

    /**
     * Reads the form submission data and extracts user information.
     *
     * This method processes the form entry and form
     * data to extract user-related
     * information such as email, first name, last name,
     * and phone number. It also
     * retrieves the address data, including city,
     * state, country, and postal code.
     *
     * If either the form entry or form data is
     * empty, an empty array is returned.
     *
     * @param array $entry The form entry data.
     * @param array $form_data The form schema data.
     *
     * @return array An associative array
     *               containing user and address information
     *               extracted from the form entry.
     */
    public static function readFormData( $entry, $form_data ) {
        if ( empty( $entry ) || empty( $form_data ) ) {
            return array();
        }

        $name = self::getName( $entry, $form_data );

        $event_data = array(
            'email'      => self::getEmail( $entry, $form_data ),
            'first_name' => ! empty( $name ) ? $name[0] : null,
            'last_name'  => ! empty( $name ) ? $name[1] : null,
            'phone'      => self::getPhone( $entry, $form_data ),
        );

        $event_data = array_merge(
            $event_data,
            self::getAddress( $entry, $form_data )
        );

        return $event_data;
    }

    /**
     * Retrieves the phone number from the form data.
     *
     * This method extracts the phone number field from the provided form entry
     * and form data.
     *
     * @param array $entry The form entry data.
     * @param array $form_data The form schema data.
     *
     * @return string|null The phone number, or null if no phone field is found.
     */
    private static function getPhone( $entry, $form_data ) {
        $phone = self::getField( $entry, $form_data, 'phone' );
        if ( ! is_null( $phone ) && '' !== $phone ) {
            return $phone;
        }

        return self::getTextFieldByLabel(
            $entry,
            $form_data,
            array( 'phone', 'tel', 'telephone', 'mobile' )
        );
    }

    /**
     * Retrieves the email address from the form data.
     *
     * This method extracts the email address field from the provided form entry
     * and form data.
     *
     * @param array $entry The form entry data.
     * @param array $form_data The form schema data.
     *
     * @return string|null The email address, or null
     *                     if no email field is found.
     */
    private static function getEmail( $entry, $form_data ) {
        return self::getField( $entry, $form_data, 'email' );
    }

    /**
     * Retrieves the address data from the form data.
     *
     * This method extracts the address data (city, state, country, and zip)
     * from the provided form entry
     * and form data. The country is sent in ISO format.
     *
     * Note that if the address scheme is 'us' and country
     * is not present, 'US' is used as the country.
     *
     * @param array $entry The form entry data.
     * @param array $form_data The form schema data.
     *
     * @return array The address data.
     */
    private static function getAddress( $entry, $form_data ) {
        $address_field_data = self::getField( $entry, $form_data, 'address' );
        if ( is_null( $address_field_data ) ) {
            // Fall back to individual text fields when the Address fancy field
            // is not available in WPForms Lite.
            return self::getAddressFromTextFields( $entry, $form_data );
        }

        $address_data = array();
        if ( isset( $address_field_data['city'] ) ) {
            $address_data['city'] = $address_field_data['city'];
        }

        if ( isset( $address_field_data['state'] ) ) {
            $address_data['state'] = $address_field_data['state'];
        }

        if ( isset( $address_field_data['country'] ) ) {
            $address_data['country'] = $address_field_data['country'];
        } else {
            $address_scheme = self::getAddressScheme( $form_data );
            if ( 'us' === $address_scheme ) {
                $address_data['country'] = 'US';
            }
        }

        if ( isset( $address_field_data['postal'] ) ) {
            $address_data['zip'] = $address_field_data['postal'];
        }

        return $address_data;
    }

    /**
     * Retrieves the user's name from the form data.
     *
     * This method extracts the name field from the provided form entry
     * and form data. It supports two formats:
     * - 'simple': where the name is a single string,
     * split into first and last name.
     * - 'first-last': where the name is provided as separate
     * 'first' and 'last' fields.
     *
     * @param array $entry The form entry data.
     * @param array $form_data The form schema data.
     *
     * @return array|null An array containing the first and
     *                    last name, or null if no name field is found.
     */
    private static function getName( $entry, $form_data ) {
        if ( empty( $form_data['fields'] ) || empty( $entry['fields'] ) ) {
            return null;
        }

        $entries = $entry['fields'];
        foreach ( $form_data['fields'] as $field ) {
            if ( 'name' === $field['type'] ) {
                if ( 'simple' === $field['format'] ) {
                    return ServerEventFactory::split_name(
                        $entries[ $field['id'] ]
                    );
                } elseif ( 'first-last' === $field['format'] ) {
                    return array(
                        $entries[ $field['id'] ]['first'],
                        $entries[ $field['id'] ]['last'],
                    );
                }
            }
        }

        return null;
    }

    /**
     * Retrieves the value of a specific field type from the form entry data.
     *
     * This method searches through the form schema data to find a field of
     * the specified type and returns the corresponding value from the form
     * entry data.
     *
     * @param array  $entry The form entry data.
     * @param array  $form_data The form schema data.
     * @param string $type The type of the field to retrieve.
     *
     * @return mixed|null The value of the field, or null if no
     *                    field of the specified type is found.
     */
    private static function getField( $entry, $form_data, $type ) {
        if ( empty( $form_data['fields'] ) || empty( $entry['fields'] ) ) {
            return null;
        }

        foreach ( $form_data['fields'] as $field ) {
            if ( $field['type'] === $type ) {
                return $entry['fields'][ $field['id'] ];
            }
        }

        return null;
    }

    /**
     * Retrieves a text field value by matching its label.
     *
     * WPForms Lite users often rely on generic "text" fields instead of
     * the premium/fancy types. This helper lets us recover values for
     * phone/address-like fields when their labels match expected names.
     *
     * @param array    $entry   The form entry data.
     * @param array    $form_data The form schema data.
     * @param string[] $labels Candidate labels (case-insensitive).
     * @return string|null
     */
    private static function getTextFieldByLabel( $entry, $form_data, $labels ) {
        if ( empty( $form_data['fields'] ) || empty( $entry['fields'] ) ) {
            return null;
        }

        $normalized_labels = array_map( array( self::class, 'normalizeLabel' ), $labels );

        foreach ( $form_data['fields'] as $field ) {
            if ( 'text' !== $field['type'] || empty( $field['label'] ) ) {
                continue;
            }

            $label = self::normalizeLabel( $field['label'] );
            if ( in_array( $label, $normalized_labels, true ) ) {
                $value = isset( $entry['fields'][ $field['id'] ] )
                    ? $entry['fields'][ $field['id'] ]
                    : null;

                return '' !== $value ? $value : null;
            }
        }

        return null;
    }

    /**
     * Builds address data from individual text fields when the Address field
     * isn't present.
     *
     * @param array $entry     The form entry data.
     * @param array $form_data The form schema data.
     *
     * @return array
     */
    private static function getAddressFromTextFields( $entry, $form_data ) {
        $address_data = array();

        $address_data['city'] = self::getTextFieldByLabel(
            $entry,
            $form_data,
            array( 'city', 'town' )
        );

        $address_data['state'] = self::getTextFieldByLabel(
            $entry,
            $form_data,
            array( 'state', 'province', 'region', 'county' )
        );

        $address_data['country'] = self::getTextFieldByLabel(
            $entry,
            $form_data,
            array( 'country', 'country/region' )
        );

        $address_data['zip'] = self::getTextFieldByLabel(
            $entry,
            $form_data,
            array( 'zip', 'postal', 'postcode', 'zip code' )
        );

        // Remove null/empty values so we don't send sparse keys.
        return array_filter(
            $address_data,
            function ( $value ) {
                return ! is_null( $value ) && '' !== $value;
            }
        );
    }

    /**
     * Normalizes labels for case-insensitive comparison.
     *
     * @param string $label The label to normalize.
     * @return string
     */
    private static function normalizeLabel( $label ) {
        return strtolower( trim( $label ) );
    }

    /**
     * Retrieves the address scheme from the form data.
     *
     * This method searches through the form schema data to find the first
     * 'address' field and returns its 'scheme' value, which is either 'us' or
     * 'international'. If no address field is found, or if the address field
     * does not have a scheme, this method returns null.
     *
     * @param array $form_data The form schema data.
     *
     * @return string|null The address scheme, or
     *                     null if no address field is found.
     */
    private static function getAddressScheme( $form_data ) {
        foreach ( $form_data['fields'] as $field ) {
            if ( 'address' === $field['type'] ) {
                if ( isset( $field['scheme'] ) ) {
                    return $field['scheme'];
                }
            }
        }
        return null;
    }
}
