<?php
/**
 * 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.
 *
 * @package FacebookPixelPlugin
 */

namespace FacebookPixelPlugin\Core;

use FacebookPixelPlugin\FacebookAds\Object\ServerSide\Event;
use FacebookPixelPlugin\FacebookAds\Object\ServerSide\UserData;
use FacebookPixelPlugin\FacebookAds\Object\ServerSide\CustomData;
use FacebookPixelPlugin\FacebookAds\Object\ServerSide\Content;
use FacebookPixelPlugin\FacebookAds\Object\ServerSide\Normalizer;

use FacebookPixelPlugin\Core\AAMFieldsExtractor;
use FacebookPixelPlugin\Core\AAMSettingsFields;
use FacebookPixelPlugin\Core\EventIdGenerator;
use FacebookPixelPlugin\Core\FacebookWordpressOptions;

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

/**
 * Class ServerEventFactory
 */
class ServerEventFactory {
    /**
     * Returns a new Event with the given name, populated
     * with the current request's
     *
     * User agent, IP address, and Facebook Browser IDs,
     * as well as the current time and a unique event ID.
     *
     * @param string  $event_name The name of the event to create.
     * @param boolean $prefer_referrer_for_event_src Whether to
     * prefer the referrer URL over the current request URL
     * as the event source URL.
     *
     * @return Event
     */
    public static function new_event(
        $event_name,
        $prefer_referrer_for_event_src = false
    ) {
        $user_data = ( new UserData() )
                    ->setClientIpAddress( self::get_ip_address() )
                    ->setClientUserAgent( self::get_http_user_agent() )
                    ->setFbp( self::get_fbp() )
                    ->setFbc( self::get_fbc() );

        $event = ( new Event() )
                ->setEventName( $event_name )
                ->setEventTime( time() )
                ->setEventId( EventIdGenerator::guidv4() )
            ->setEventSourceUrl(
                self::get_request_uri( $prefer_referrer_for_event_src )
            )
                ->setActionSource( 'website' )
                ->setUserData( $user_data )
                ->setCustomData( new CustomData() );

        return $event;
    }

    /**
     * Scans the HTTP headers for the first valid IP address it can find.
     *
     * @return string|null The first valid IP address found, or null if none
     *                     were found.
     */
    private static function get_ip_address() {
        $headers_to_scan = array(
            'HTTP_CLIENT_IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_FORWARDED',
            'HTTP_X_CLUSTER_CLIENT_IP',
            'HTTP_FORWARDED_FOR',
            'HTTP_FORWARDED',
            'REMOTE_ADDR',
        );

        foreach ( $headers_to_scan as $header ) {
            if ( isset( $_SERVER[ $header ] ) ) {
                $ip_list = explode( ',', $_SERVER[ $header ] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
                foreach ( $ip_list as $ip ) {
                    $trimmed_ip = trim( $ip );
                    if ( self::is_valid_ip_address( $trimmed_ip ) ) {
                        return $trimmed_ip;
                    }
                }
            }
        }

        return null;
    }

    /**
     * Retrieves the User-Agent string from the HTTP request headers.
     *
     * @return string|null The User-Agent string, or null if it was not found.
     */
    private static function get_http_user_agent() {
        $user_agent = null;

        if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
            $user_agent = sanitize_text_field( $_SERVER['HTTP_USER_AGENT'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        }

        return $user_agent;
    }

    /**
     * Retrieves the request URI for the current HTTP request.
     *
     * This function constructs the full request URI by considering
     * the protocol, host, and request path. If the
     * $prefer_referrer_for_event_src parameter is true and a referrer
     * URL is present in the HTTP headers, it returns the referrer URL instead.
     *
     * @param boolean $prefer_referrer_for_event_src Whether to
     * prefer the referrer URL over the current request URL.
     *
     * @return string The constructed request URI or the referrer
     * URL if preferred.
     */
    private static function get_request_uri( $prefer_referrer_for_event_src ) {
        if ( $prefer_referrer_for_event_src
        && ! empty( $_SERVER['HTTP_REFERER'] ) ) {
            return sanitize_text_field( $_SERVER['HTTP_REFERER'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        }

            $url = 'http://';
        if ( ! empty( $_SERVER['HTTPS'] ) && 'off' !== $_SERVER['HTTPS'] ) {
            $url = 'https://';
        }

        if ( ! empty( $_SERVER['HTTP_HOST'] ) ) {
            $url .= sanitize_text_field( $_SERVER['HTTP_HOST'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        }

        if ( ! empty( $_SERVER['REQUEST_URI'] ) ) {
            $url .= sanitize_text_field( $_SERVER['REQUEST_URI'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
        }

        return $url;
    }

    /**
     * Retrieves the Facebook Browser ID (FBP).
     *
     * Resolution order:
     * 1. ParamBuilder server-side extraction
     * 2. _fbp cookie (set by fbevents.js)
     *
     * @return string|null The FBP value, or null if unavailable.
     */
    private static function get_fbp() {
        $fbp = FacebookParamBuilder::get_fbp();

        if ( empty( $fbp ) && ! empty( $_COOKIE['_fbp'] ) ) {
            $fbp = sanitize_text_field( wp_unslash( $_COOKIE['_fbp'] ) );
        }

        return $fbp;
    }

    /**
     * Public wrapper for the current request's FBP value.
     *
     * @return string|null
     */
    public static function get_fbp_value() {
        return self::get_fbp();
    }

    /**
     * Retrieves the Facebook Click ID (FBC).
     *
     * Resolution order:
     * 1. ParamBuilder server-side extraction
     * 2. _fbc cookie (if fbclid hasn't changed)
     * 3. Constructed from fbclid query parameter
     * 4. Session fallback
     *
     * @return string|null The FBC value, or null if unavailable.
     */
    private static function get_fbc() {
        $fbc = FacebookParamBuilder::get_fbc();

        if ( empty( $fbc ) ) {
            $cookie_fbc = ! empty( $_COOKIE['_fbc'] )
                ? sanitize_text_field( wp_unslash( $_COOKIE['_fbc'] ) )
                : null;

            $request_fbclid = isset( $_GET['fbclid'] ) // phpcs:ignore WordPress.Security.NonceVerification
                ? self::sanitize_fbclid(
                    wp_unslash( $_GET['fbclid'] ) // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
                )
                : null;

            if ( $request_fbclid && ( empty( $cookie_fbc ) || self::has_fbclid_changed( $cookie_fbc, $request_fbclid ) ) ) {
                $cur_time = (int) ( microtime( true ) * 1000 );
                $fbc      = 'fb.1.' . $cur_time . '.' . $request_fbclid;
            }

            if ( empty( $fbc ) && ! empty( $cookie_fbc ) ) {
                $fbc = $cookie_fbc;
            }

            if ( empty( $fbc ) && isset( $_SESSION['_fbc'] ) ) {
                $fbc = sanitize_text_field( $_SESSION['_fbc'] );
            }
        }

        if ( ! empty( $fbc ) && ! FacebookSignalState::is_held() ) {
            $_SESSION['_fbc'] = $fbc;
        }

        return $fbc;
    }

    /**
     * Public wrapper for the current request's FBC value.
     *
     * @return string|null
     */
    public static function get_fbc_value() {
        return self::get_fbc();
    }

    /**
     * Checks whether the fbclid in the current request differs from
     * the one stored in the existing _fbc cookie.
     *
     * @param string      $cookie_fbc The current _fbc cookie value
     *                                (format: fb.{subdomain}.{timestamp}.{fbclid}).
     * @param string|null $request_fbclid The fbclid from the current request,
     *                                    or null if not present.
     *
     * @return bool True if fbclid has changed, false otherwise.
     */
    private static function has_fbclid_changed( $cookie_fbc, $request_fbclid ) {
        if ( null === $request_fbclid ) {
            return false;
        }

        $parts = explode( '.', $cookie_fbc );
        if ( count( $parts ) < 4 ) {
            return true;
        }

        $cookie_fbclid = implode( '.', array_slice( $parts, 3 ) );
        return $cookie_fbclid !== $request_fbclid;
    }

    /**
     * Decodes and validates an fbclid value.
     *
     * Decodes URL encoding first, then validates against an
     * allowlist of safe characters. Returns null if the value
     * contains unexpected characters.
     *
     * @param string $raw_fbclid The raw fbclid from the query string.
     *
     * @return string|null The sanitized fbclid, or null if invalid.
     */
    private static function sanitize_fbclid( $raw_fbclid ) {
        $decoded = rawurldecode( $raw_fbclid );
        if ( preg_match( '/^[A-Za-z0-9_-]+$/', $decoded ) ) {
            return $decoded;
        }
        return null;
    }

    /**
     * Validates an IP address.
     *
     * This function takes an IP address and returns true if it is valid,
     * false otherwise. The function uses the filter_var function to validate
     * the IP address, and it filters out private and reserved IP addresses.
     *
     * @param string $ip_address The IP address to validate.
     * @return bool True if the IP address is valid, false otherwise.
     */
    private static function is_valid_ip_address( $ip_address ) {
        return filter_var(
            $ip_address,
            FILTER_VALIDATE_IP,
            FILTER_FLAG_IPV4 |
            FILTER_FLAG_IPV6 |
            FILTER_FLAG_NO_PRIV_RANGE |
                FILTER_FLAG_NO_RES_RANGE
        );
    }

    /**
     * Given that the data extracted by the integration classes is a mix of
     * user data and custom data,
     * this function splits these fields in two arrays
     * and user data is formatted with the AAM field setting
     *
     * @param array $data Data extracted by the integration.
     * @return array
     */
    private static function split_user_data_and_custom_data( $data ) {
        $user_data        = array();
        $custom_data      = array();
        $key_to_aam_field = array(
            'email'         => AAMSettingsFields::EMAIL,
            'first_name'    => AAMSettingsFields::FIRST_NAME,
            'last_name'     => AAMSettingsFields::LAST_NAME,
            'phone'         => AAMSettingsFields::PHONE,
            'state'         => AAMSettingsFields::STATE,
            'country'       => AAMSettingsFields::COUNTRY,
            'city'          => AAMSettingsFields::CITY,
            'zip'           => AAMSettingsFields::ZIP_CODE,
            'gender'        => AAMSettingsFields::GENDER,
            'date_of_birth' => AAMSettingsFields::DATE_OF_BIRTH,
            'external_id'   => AAMSettingsFields::EXTERNAL_ID,
        );
        foreach ( $data as $key => $value ) {
            if ( isset( $key_to_aam_field[ $key ] ) ) {
                $user_data[ $key_to_aam_field[ $key ] ] = $value;
            } else {
                $custom_data[ $key ] = $value;
            }
        }
        return array(
            'user_data'   => $user_data,
            'custom_data' => $custom_data,
        );
    }

    /**
     * Given a callback and its arguments, it calls the callback
     * with the arguments and extracts the user data and custom
     * data from the result.
     *
     * It uses the AAM setting to normalize the user data and the custom data
     * is used as is.
     *
     * If an exception is thrown in the callback, it's caught and logged,
     * and the function returns an empty Event object.
     *
     * @param string   $event_name The name of the event.
     * @param callable $callback The callback to call.
     * @param array    $arguments The arguments to pass to the callback.
     * @param string   $integration The integration name.
     * @param boolean  $prefer_referrer_for_event_src Whether to prefer
     * the referrer URL over the current request URL as the event source URL.
     *
     * @return Event The event object.
     *
     * @throws \Exception If there was an preprocessing error.
     */
    public static function safe_create_event(
        $event_name,
        $callback,
        $arguments,
        $integration,
        $prefer_referrer_for_event_src = false
    ) {
        $event = self::new_event( $event_name, $prefer_referrer_for_event_src );

        $data              = call_user_func_array( $callback, $arguments );
        $data_split        = self::split_user_data_and_custom_data( $data );
        $user_data_array   = $data_split['user_data'];
        $custom_data_array = $data_split['custom_data'];
        $user_data_array   = AAMFieldsExtractor::get_normalized_user_data(
            $user_data_array
        );

        $user_data = $event->getUserData();
        if ( isset( $data['fbp'] ) ) {
            $user_data->setFbp( $data['fbp'] );
        }
        if ( isset( $data['fbc'] ) ) {
            $user_data->setFbc( $data['fbc'] );
        }
        if (
        isset( $user_data_array[ AAMSettingsFields::EMAIL ] )
        ) {
            $user_data->setEmail(
                $user_data_array[ AAMSettingsFields::EMAIL ]
            );
        }
        if (
        isset( $user_data_array[ AAMSettingsFields::FIRST_NAME ] )
        ) {
            $user_data->setFirstName(
                $user_data_array[ AAMSettingsFields::FIRST_NAME ]
            );
        }
        if (
        isset( $user_data_array[ AAMSettingsFields::LAST_NAME ] )
        ) {
            $user_data->setLastName(
                $user_data_array[ AAMSettingsFields::LAST_NAME ]
            );
        }
        if (
        isset( $user_data_array[ AAMSettingsFields::GENDER ] )
        ) {
            $user_data->setGender(
                $user_data_array[ AAMSettingsFields::GENDER ]
            );
        }
        if (
        isset( $user_data_array[ AAMSettingsFields::DATE_OF_BIRTH ] )
        ) {
            $user_data->setDateOfBirth(
                $user_data_array[ AAMSettingsFields::DATE_OF_BIRTH ]
            );
        }
        if (
        isset( $user_data_array[ AAMSettingsFields::EXTERNAL_ID ] ) &&
        ! is_null( $user_data_array[ AAMSettingsFields::EXTERNAL_ID ] )
        ) {
            if ( is_array( $user_data_array[ AAMSettingsFields::EXTERNAL_ID ] ) ) {
                $external_ids = $user_data_array[ AAMSettingsFields::EXTERNAL_ID ];
                $hashed_eids  = array();
                foreach ( $external_ids as $k => $v ) {
                    $hashed_eids[ $k ] = hash( 'sha256', $v );
                }
                $user_data->setExternalIds( $hashed_eids );
            } else {
                $user_data->setExternalId(
                    hash(
                        'sha256',
                        $user_data_array[ AAMSettingsFields::EXTERNAL_ID ]
                    )
                );
            }
        }
        if (
        isset( $user_data_array[ AAMSettingsFields::PHONE ] )
        ) {
            $user_data->setPhone(
                $user_data_array[ AAMSettingsFields::PHONE ]
            );
        }
        if (
        isset( $user_data_array[ AAMSettingsFields::CITY ] )
        ) {
            $user_data->setCity(
                $user_data_array[ AAMSettingsFields::CITY ]
            );
        }
        if (
        isset( $user_data_array[ AAMSettingsFields::STATE ] )
        ) {
            $user_data->setState(
                $user_data_array[ AAMSettingsFields::STATE ]
            );
        }
        if (
        isset( $user_data_array[ AAMSettingsFields::ZIP_CODE ] )
        ) {
            $user_data->setZipCode(
                $user_data_array[ AAMSettingsFields::ZIP_CODE ]
            );
        }
        if (
        isset( $user_data_array[ AAMSettingsFields::COUNTRY ] )
        ) {
            $user_data->setCountryCode(
                $user_data_array[ AAMSettingsFields::COUNTRY ]
            );
        }

        $custom_data = $event->getCustomData();
        $custom_data->addCustomProperty(
            'fb_integration_tracking',
            $integration
        );

        if ( ! empty( $data['currency'] ) ) {
            $custom_data->setCurrency( $custom_data_array['currency'] );
        }

        if ( ! empty( $data['value'] ) ) {
            $custom_data->setValue( $custom_data_array['value'] );
        }

        if ( ! empty( $data['contents'] ) ) {
            $custom_data->setContents( $custom_data_array['contents'] );
        }

        if ( ! empty( $data['content_ids'] ) ) {
            $custom_data->setContentIds( $custom_data_array['content_ids'] );
        }

        if ( ! empty( $data['content_type'] ) ) {
            $custom_data->setContentType( $custom_data_array['content_type'] );
        }

        if ( ! empty( $data['num_items'] ) ) {
            $custom_data->setNumItems( $custom_data_array['num_items'] );
        }

        if ( ! empty( $data['content_name'] ) ) {
            $custom_data->setContentName( $custom_data_array['content_name'] );
        }

        if ( ! empty( $data['content_category'] ) ) {
        $custom_data->setContentCategory(
            $custom_data_array['content_category']
        );
        }

        return $event;
    }

    /**
     * Split a full name string into an array containing the first name
     * and last name.
     *
     * If the name contains a space, it will be split into a first name and
     * last name. Otherwise, the entire name will be considered the first
     * name and the last name will be null.
     *
     * @param string $name The full name to split.
     * @return array An array containing the first name and last name.
     */
    public static function split_name( $name ) {
        $first_name = $name;
        $last_name  = null;
        $index      = strpos( $name, ' ' );
        if ( false !== $index ) {
            $first_name = substr( $name, 0, $index );
            $last_name  = substr( $name, $index + 1 );
        }

        return array( $first_name, $last_name );
    }

    /**
     * Converts a raw event array (Graph API format) into an Event object.
     *
     * @param array $event_as_array The event in array form.
     * @return Event
     */
    public static function create_from_array( $event_as_array ) {
        $event = new Event( $event_as_array );
        if ( isset( $event_as_array['user_data'] ) ) {
            $user_data = new UserData(
                self::map_user_data_keys( $event_as_array['user_data'] )
            );
            $event->setUserData( $user_data );
        }
        if ( isset( $event_as_array['custom_data'] ) ) {
            $custom_data = new CustomData( $event_as_array['custom_data'] );
            if ( isset( $event_as_array['custom_data']['contents'] ) ) {
                $contents = array();
                foreach (
                    $event_as_array['custom_data']['contents']
                    as $contents_as_array
                ) {
                    if ( isset( $contents_as_array['id'] ) ) {
                        $contents_as_array['product_id'] =
                            $contents_as_array['id'];
                    }
                    $contents[] = new Content( $contents_as_array );
                }
                $custom_data->setContents( $contents );
            }
            if ( isset(
                $event_as_array['custom_data']['fb_integration_tracking']
            ) ) {
                $custom_data->addCustomProperty(
                    'fb_integration_tracking',
                    $event_as_array['custom_data']['fb_integration_tracking']
                );
            }
            $event->setCustomData( $custom_data );
        }
        return $event;
    }

    /**
     * Maps normalized user-data keys to UserData constructor keys.
     *
     * @param array $user_data_normalized Normalized user data.
     * @return array Mapped user data.
     */
    public static function map_user_data_keys( $user_data_normalized ) {
        $norm_key_to_key = array(
            AAMSettingsFields::EMAIL         => 'emails',
            AAMSettingsFields::FIRST_NAME    => 'first_names',
            AAMSettingsFields::LAST_NAME     => 'last_names',
            AAMSettingsFields::GENDER        => 'genders',
            AAMSettingsFields::DATE_OF_BIRTH => 'dates_of_birth',
            AAMSettingsFields::EXTERNAL_ID   => 'external_ids',
            AAMSettingsFields::PHONE         => 'phones',
            AAMSettingsFields::CITY          => 'cities',
            AAMSettingsFields::STATE         => 'states',
            AAMSettingsFields::ZIP_CODE      => 'zip_codes',
            AAMSettingsFields::COUNTRY       => 'country_codes',
        );
        $user_data       = array();
        foreach ( $user_data_normalized as $norm_key => $field ) {
            if ( isset( $norm_key_to_key[ $norm_key ] ) ) {
                $user_data[ $norm_key_to_key[ $norm_key ] ] = $field;
            } else {
                $user_data[ $norm_key ] = $field;
            }
        }
        return $user_data;
    }
}
